# Know Foolery - Development Guidelines & Coding Standards ## Overview This document establishes coding standards, development practices, and guidelines for the Know Foolery project to ensure consistency, maintainability, and quality across the codebase. ## Code Quality Standards ### General Code Quality Orientations - **Type Safety**: TypeScript for frontend, Go's type system for backend - **Code Review**: Mandatory peer review for all changes - **Static Analysis**: Automated code quality checks - **Documentation**: Comprehensive API and component documentation ### Linting and Formatting ```yaml # .golangci.yml run: timeout: 5m tests: true linters: enable: - gofmt - goimports - govet - errcheck - staticcheck - unused - gosimple - structcheck - varcheck - ineffassign - deadcode - typecheck - gosec - misspell - lll - unconvert - dupl - goconst - gocyclo linters-settings: lll: line-length: 120 gocyclo: min-complexity: 15 dupl: threshold: 150 issues: exclude-rules: - path: _test\.go linters: - gocyclo - errcheck - dupl - gosec ``` ```json // .eslintrc.json { "extends": [ "plugin:solid/typescript", "@typescript-eslint/recommended", "prettier" ], "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint", "solid"], "rules": { "@typescript-eslint/explicit-function-return-type": "error", "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/prefer-const": "error", "solid/reactivity": "warn", "prefer-const": "error", "no-var": "error", "object-shorthand": "error", "prefer-template": "error" } } ``` ### Naming Conventions #### Go Code ```go // Files: snake_case user_service.go jwt_middleware.go database_client.go // Packages: lowercase, single word when possible package auth package database package observability // Exported types: PascalCase type GameService struct {} type AuthClaims struct {} type DatabaseConfig struct {} // Unexported types: camelCase type userRepository struct {} type tokenValidator struct {} // Functions: PascalCase (exported), camelCase (unexported) func NewGameService() *GameService {} func (s *GameService) StartGame() error {} func validateToken() error {} // Constants: PascalCase or SCREAMING_SNAKE_CASE for groups const DefaultTimeout = 30 * time.Second const ( StatusActive = "active" StatusInactive = "inactive" ) // Interfaces: noun or adjective + "er" suffix type TokenValidator interface {} type UserRepository interface {} type ScoreCalculator interface {} ``` #### TypeScript Code ```typescript // Files: kebab-case game-card.tsx user-service.ts auth-store.ts // Components: PascalCase export const GameCard: Component = () => {} export const LeaderboardList: Component = () => {} // Composables/stores: camelCase with "create"/"use" prefix export const createGameSessionStore = () => {} export const useAuthentication = () => {} // Functions: camelCase export const calculateScore = () => {} export const validateAnswer = () => {} // Types/Interfaces: PascalCase interface GameSessionProps {} type AuthenticationState = 'loading' | 'authenticated' | 'unauthenticated' // Constants: SCREAMING_SNAKE_CASE export const MAX_ATTEMPTS = 3 export const SESSION_TIMEOUT = 30 * 60 * 1000 // 30 minutes ``` ## Go Development Standards ### Fiber Framework Patterns #### Service Structure ```go // Service initialization with Fiber package main import ( "log" "github.com/gofiber/fiber/v3" "github.com/gofiber/fiber/v3/middleware/cors" "github.com/gofiber/fiber/v3/middleware/logger" "github.com/gofiber/fiber/v3/middleware/recover" ) func main() { // Create Fiber app with configuration app := fiber.New(fiber.Config{ AppName: "Know Foolery Game Service", ServerHeader: "Fiber", ErrorHandler: customErrorHandler, ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, IdleTimeout: time.Second * 120, }) // Global middleware app.Use(logger.New()) app.Use(recover.New()) app.Use(cors.New(cors.Config{ AllowOrigins: "http://localhost:3000, https://app.knowfoolery.com", AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", AllowHeaders: "Origin, Content-Type, Authorization", })) // Health check endpoint app.Get("/health", healthCheck) // API routes with versioning api := app.Group("/api/v1") setupGameRoutes(api) // Start server log.Fatal(app.Listen(":8080")) } func customErrorHandler(c *fiber.Ctx, err error) error { code := fiber.StatusInternalServerError message := "Internal Server Error" if e, ok := err.(*fiber.Error); ok { code = e.Code message = e.Message } return c.Status(code).JSON(fiber.Map{ "error": true, "message": message, "code": code, }) } ``` #### Handler Patterns ```go // Handler structure and patterns package handlers import ( "github.com/gofiber/fiber/v3" "knowfoolery/internal/services" ) type GameHandler struct { gameService *services.GameService validator *validator.Validate } func NewGameHandler(gameService *services.GameService) *GameHandler { return &GameHandler{ gameService: gameService, validator: validator.New(), } } // HTTP handler with proper error handling func (h *GameHandler) StartGame(c *fiber.Ctx) error { var req StartGameRequest // Parse and validate request if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": true, "message": "Invalid request body", }) } if err := h.validator.Struct(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": true, "message": "Validation failed", "details": err.Error(), }) } // Extract user context from JWT middleware userID := c.Locals("user_id").(string) userRole := c.Locals("user_role").(string) // Business logic gameSession, err := h.gameService.StartGame(c.Context(), userID, req.PlayerName) if err != nil { return handleServiceError(c, err) } return c.Status(fiber.StatusCreated).JSON(fiber.Map{ "success": true, "data": gameSession, }) } // Centralized error handling func handleServiceError(c *fiber.Ctx, err error) error { switch { case errors.Is(err, services.ErrInvalidInput): return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": true, "message": "Invalid input", "details": err.Error(), }) case errors.Is(err, services.ErrNotFound): return c.Status(fiber.StatusNotFound).JSON(fiber.Map{ "error": true, "message": "Resource not found", }) case errors.Is(err, services.ErrUnauthorized): return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Unauthorized access", }) default: return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": true, "message": "Internal server error", }) } } // Request/Response structures with validation type StartGameRequest struct { PlayerName string `json:"player_name" validate:"required,min=2,max=50,alphanum_space"` } type StartGameResponse struct { SessionID string `json:"session_id"` PlayerName string `json:"player_name"` StartTime time.Time `json:"start_time"` TimeRemaining int `json:"time_remaining_seconds"` } ``` #### Middleware Patterns ```go // Custom middleware for authentication package middleware import ( "github.com/gofiber/fiber/v3" "knowfoolery/shared/auth" ) func JWTMiddleware(zitadelRepo auth.ZitadelRepository) fiber.Handler { return func(c *fiber.Ctx) error { // Extract token from Authorization header authHeader := c.Get("Authorization") if authHeader == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Authorization header required", }) } token := strings.TrimPrefix(authHeader, "Bearer ") if token == authHeader { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Invalid authorization header format", }) } // Validate token claims, err := zitadelRepo.ValidateToken(c.Context(), token) if err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{ "error": true, "message": "Invalid token", }) } // Set user context c.Locals("user_id", claims.Subject) c.Locals("user_email", claims.Email) c.Locals("user_name", claims.Name) c.Locals("user_roles", claims.Roles) return c.Next() } } // Rate limiting middleware func RateLimitMiddleware(redisClient *redis.Client) fiber.Handler { return func(c *fiber.Ctx) error { userID := c.Locals("user_id") clientIP := c.IP() // Per-user rate limiting if userID != nil { userKey := fmt.Sprintf("rate_limit:user:%s", userID) if !allowRequest(redisClient, userKey, 60, time.Minute) { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "error": true, "message": "Rate limit exceeded for user", }) } } // Per-IP rate limiting ipKey := fmt.Sprintf("rate_limit:ip:%s", clientIP) if !allowRequest(redisClient, ipKey, 100, time.Minute) { return c.Status(fiber.StatusTooManyRequests).JSON(fiber.Map{ "error": true, "message": "Rate limit exceeded for IP", }) } return c.Next() } } ``` ### Database Patterns with Ent #### Schema Definition Standards ```go // Ent schema with proper field validation and indexes package schema import ( "entgo.io/ent" "entgo.io/ent/schema/edge" "entgo.io/ent/schema/field" "entgo.io/ent/schema/index" "time" ) type Question struct { ent.Schema } func (Question) Fields() []ent.Field { return []ent.Field{ field.String("theme"). NotEmpty(). MaxLen(100). Comment("Question category/theme"), field.Text("text"). NotEmpty(). MaxLen(1000). Comment("The actual question text"), field.String("answer"). NotEmpty(). MaxLen(500). Comment("Correct answer"), field.Text("hint"). Optional(). MaxLen(500). Comment("Optional hint text"), field.Enum("difficulty"). Values("easy", "medium", "hard"). Default("medium"). Comment("Question difficulty level"), field.Bool("is_active"). Default(true). Comment("Whether question is active"), field.Time("created_at"). Default(time.Now). Immutable(). Comment("Creation timestamp"), field.Time("updated_at"). Default(time.Now). UpdateDefault(time.Now). Comment("Last update timestamp"), } } func (Question) Indexes() []ent.Index { return []ent.Index{ index.Fields("theme", "is_active"), index.Fields("difficulty", "is_active"), index.Fields("created_at"), } } func (Question) Edges() []ent.Edge { return []ent.Edge{ edge.To("attempts", QuestionAttempt.Type), } } ``` #### Service Layer Patterns ```go // Service layer with proper error handling package services import ( "context" "errors" "knowfoolery/internal/models" "knowfoolery/ent" ) var ( ErrInvalidInput = errors.New("invalid input") ErrNotFound = errors.New("resource not found") ErrUnauthorized = errors.New("unauthorized access") ErrGameInProgress = errors.New("game already in progress") ErrSessionExpired = errors.New("session expired") ) type GameService struct { client *ent.Client questionSvc QuestionService sessionSvc SessionService validator *validator.Validate } func NewGameService( client *ent.Client, questionSvc QuestionService, sessionSvc SessionService, ) *GameService { return &GameService{ client: client, questionSvc: questionSvc, sessionSvc: sessionSvc, validator: validator.New(), } } func (s *GameService) StartGame(ctx context.Context, userID, playerName string) (*models.GameSession, error) { // Validate input if err := s.validatePlayerName(playerName); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidInput, err) } // Check for existing active session activeSession, err := s.client.GameSession. Query(). Where(gamesession.And( gamesession.PlayerNameEQ(playerName), gamesession.StatusEQ(gamesession.StatusActive), )). Only(ctx) if err != nil && !ent.IsNotFound(err) { return nil, fmt.Errorf("failed to check existing session: %w", err) } if activeSession != nil { return nil, ErrGameInProgress } // Start database transaction tx, err := s.client.Tx(ctx) if err != nil { return nil, fmt.Errorf("failed to start transaction: %w", err) } defer tx.Rollback() // Create new game session session, err := tx.GameSession. Create(). SetPlayerName(playerName). SetTotalScore(0). SetQuestionsAsked(0). SetQuestionsCorrect(0). SetHintsUsed(0). SetStartTime(time.Now()). SetStatus(gamesession.StatusActive). Save(ctx) if err != nil { return nil, fmt.Errorf("failed to create session: %w", err) } // Get first question question, err := s.questionSvc.GetRandomQuestion(ctx) if err != nil { return nil, fmt.Errorf("failed to get question: %w", err) } // Update session with first question session, err = session.Update(). SetCurrentQuestionID(question.ID). SetCurrentAttempts(0). Save(ctx) if err != nil { return nil, fmt.Errorf("failed to update session: %w", err) } // Commit transaction if err := tx.Commit(); err != nil { return nil, fmt.Errorf("failed to commit transaction: %w", err) } return &models.GameSession{ ID: session.ID, PlayerName: session.PlayerName, TotalScore: session.TotalScore, StartTime: session.StartTime, Status: string(session.Status), CurrentQuestion: &models.Question{ ID: question.ID, Theme: question.Theme, Text: question.Text, Hint: question.Hint, }, TimeRemaining: s.calculateTimeRemaining(session.StartTime), }, nil } func (s *GameService) validatePlayerName(name string) error { type PlayerNameValidation struct { Name string `validate:"required,min=2,max=50,alphanum_space"` } return s.validator.Struct(&PlayerNameValidation{Name: name}) } func (s *GameService) calculateTimeRemaining(startTime time.Time) int { const sessionDuration = 30 * time.Minute elapsed := time.Since(startTime) remaining := sessionDuration - elapsed if remaining < 0 { return 0 } return int(remaining.Seconds()) } ``` ## TypeScript Development Standards ### SolidJS Component Patterns #### Component Structure ```typescript // Component with proper TypeScript types and SolidJS patterns import type { Component } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js' import { Box, Button, Input, LinearProgress, Typography, Chip, } from '@suid/material' // Props interface with proper documentation export interface GameCardProps { /** The question to display */ question: string /** Theme/category of the question */ theme: string /** Time remaining in seconds */ timeRemaining: number /** Number of attempts left */ attemptsLeft: number /** Callback when answer is submitted */ onSubmitAnswer: (answer: string) => Promise /** Loading state from async operations */ loading?: boolean } /** * GameCard component displays a quiz question with input and controls * * @param props - GameCard props * @returns SolidJS component */ export const GameCard: Component = (props) => { const [answer, setAnswer] = createSignal('') const [isSubmitting, setIsSubmitting] = createSignal(false) const disabled = createMemo(() => props.loading || isSubmitting()) const progressValue = createMemo(() => ((30 * 60 - props.timeRemaining) / (30 * 60)) * 100) const handleSubmit = async () => { const value = answer().trim() if (!value || disabled()) return setIsSubmitting(true) try { await props.onSubmitAnswer(value) setAnswer('') } catch (error) { console.error('Failed to submit answer:', error) } finally { setIsSubmitting(false) } } return ( {props.timeRemaining}s {props.question} Attempts left: {props.attemptsLeft}/3 setAnswer(e.currentTarget.value)} disabled={disabled()} placeholder="Enter your answer" /> Submitting... ) } ``` #### Solid Composable Pattern ```typescript import { createSignal } from 'solid-js' import { GameSessionService } from '../services/game-session-service' export const createGameSessionStore = (playerName: () => string) => { const service = new GameSessionService() const [isLoading, setIsLoading] = createSignal(false) const [error, setError] = createSignal(null) const [sessionId, setSessionId] = createSignal(null) const startGame = async () => { setIsLoading(true) setError(null) try { const session = await service.startGame(playerName()) setSessionId(session.sessionId) return session } catch (err) { const message = err instanceof Error ? err.message : 'Failed to start game' setError(message) throw err } finally { setIsLoading(false) } } return { isLoading, error, sessionId, startGame, } } ``` ## Testing Standards ### Go Testing Patterns ```go // Comprehensive testing with testify and testcontainers package services_test import ( "context" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/testcontainers/testcontainers-go/modules/postgres" "knowfoolery/internal/services" "knowfoolery/ent/enttest" ) type GameServiceTestSuite struct { suite.Suite ctx context.Context gameService *services.GameService client *ent.Client } func (suite *GameServiceTestSuite) SetupSuite() { suite.ctx = context.Background() // Start PostgreSQL container for integration tests postgresContainer, err := postgres.RunContainer(suite.ctx, testcontainers.WithImage("postgres:15-alpine"), postgres.WithDatabase("testdb"), postgres.WithUsername("testuser"), postgres.WithPassword("testpass"), ) require.NoError(suite.T(), err) connStr, err := postgresContainer.ConnectionString(suite.ctx) require.NoError(suite.T(), err) // Create Ent client with test database suite.client = enttest.Open(suite.T(), "postgres", connStr) // Initialize service suite.gameService = services.NewGameService( suite.client, services.NewQuestionService(suite.client), services.NewSessionService(suite.client), ) } func (suite *GameServiceTestSuite) TearDownSuite() { suite.client.Close() } func (suite *GameServiceTestSuite) SetupTest() { // Clean database before each test suite.client.GameSession.Delete().ExecX(suite.ctx) suite.client.Question.Delete().ExecX(suite.ctx) // Seed test data suite.seedTestQuestions() } func (suite *GameServiceTestSuite) TestStartGame_Success() { // Arrange playerName := "TestPlayer" // Act session, err := suite.gameService.StartGame(suite.ctx, "user123", playerName) // Assert require.NoError(suite.T(), err) assert.NotEmpty(suite.T(), session.ID) assert.Equal(suite.T(), playerName, session.PlayerName) assert.Equal(suite.T(), 0, session.TotalScore) assert.NotNil(suite.T(), session.CurrentQuestion) assert.True(suite.T(), session.TimeRemaining > 0) } func (suite *GameServiceTestSuite) TestStartGame_InvalidPlayerName() { // Test cases for validation testCases := []struct { name string playerName string expectedErr string }{ {"empty name", "", "required"}, {"too short", "a", "min"}, {"too long", strings.Repeat("a", 51), "max"}, {"invalid chars", "player@123", "alphanum_space"}, } for _, tc := range testCases { suite.T().Run(tc.name, func(t *testing.T) { _, err := suite.gameService.StartGame(suite.ctx, "user123", tc.playerName) require.Error(t, err) assert.Contains(t, err.Error(), tc.expectedErr) }) } } func (suite *GameServiceTestSuite) TestStartGame_ExistingActiveSession() { // Arrange playerName := "TestPlayer" // Start first game _, err := suite.gameService.StartGame(suite.ctx, "user123", playerName) require.NoError(suite.T(), err) // Act - try to start second game _, err = suite.gameService.StartGame(suite.ctx, "user123", playerName) // Assert require.Error(suite.T(), err) assert.Equal(suite.T(), services.ErrGameInProgress, err) } func (suite *GameServiceTestSuite) seedTestQuestions() { questions := []struct { theme string text string answer string hint string }{ {"Geography", "What is the capital of France?", "Paris", "City of Light"}, {"History", "Who painted the Mona Lisa?", "Leonardo da Vinci", "Renaissance artist"}, {"Science", "What is the largest planet?", "Jupiter", "Gas giant"}, } for _, q := range questions { suite.client.Question.Create(). SetTheme(q.theme). SetText(q.text). SetAnswer(q.answer). SetHint(q.hint). SetIsActive(true). ExecX(suite.ctx) } } func TestGameServiceSuite(t *testing.T) { suite.Run(t, new(GameServiceTestSuite)) } // Benchmark tests func BenchmarkGameService_StartGame(b *testing.B) { // Setup ctx := context.Background() client := enttest.Open(b, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1") defer client.Close() gameService := services.NewGameService(client, nil, nil) // Benchmark b.ResetTimer() for i := 0; i < b.N; i++ { playerName := fmt.Sprintf("Player%d", i) _, _ = gameService.StartGame(ctx, fmt.Sprintf("user%d", i), playerName) } } ``` ### TypeScript Testing Patterns ```typescript // SolidJS component testing with Vitest and Testing Library import { render, screen, fireEvent } from '@solidjs/testing-library' import { describe, expect, it, vi } from 'vitest' import { GameCard, GameCardProps } from '../GameCard' // Mock props factory const createMockProps = (overrides: Partial = {}): GameCardProps => ({ question: "What is the capital of France?", theme: "Geography", timeRemaining: 1800, // 30 minutes attemptsLeft: 3, onSubmitAnswer: vi.fn(), loading: false, ...overrides, }) describe('GameCard', () => { describe('Rendering', () => { it('should render question and theme correctly', () => { const props = createMockProps() render(() => ) expect(screen.getByText(props.question)).toBeInTheDocument() expect(screen.getByText(props.theme)).toBeInTheDocument() }) }) describe('User Interactions', () => { it('should call onSubmitAnswer when submit button is clicked', async () => { const props = createMockProps() const mockOnSubmitAnswer = vi.fn().mockResolvedValue(undefined) props.onSubmitAnswer = mockOnSubmitAnswer render(() => ) const input = screen.getByPlaceholderText('Enter your answer') fireEvent.input(input, { target: { value: 'Paris' } }) const submitButton = screen.getByRole('button', { name: 'Submit Answer' }) fireEvent.click(submitButton) expect(mockOnSubmitAnswer).toHaveBeenCalledWith('Paris') }) }) }) ``` ## Git Workflow ```bash # Branch naming conventions feature/add-game-session-management bugfix/fix-timer-display-issue hotfix/security-vulnerability-patch refactor/improve-database-queries # Commit message format type(scope): description # Types: feat, fix, docs, style, refactor, test, chore # Examples: feat(game): add session timeout handling fix(auth): resolve JWT token validation issue docs(api): update authentication endpoint documentation test(services): add comprehensive game service tests ``` ## Development Commands ### Backend Development ```bash # Navigate to service directory first cd backend/services/{service-name} # Start service in development mode with hot reload go run cmd/main.go # Service ports are env-driven via shared bootstrap helpers: # ADMIN_SERVICE_PORT, GAME_SESSION_PORT, GATEWAY_PORT, # LEADERBOARD_PORT, QUESTION_BANK_PORT, USER_SERVICE_PORT # Run tests for a specific service go test ./... -v # Run tests with coverage go test ./... -cover # Build service binary go build -o bin/{service-name} cmd/main.go # Generate Ent schemas (from service root) go generate ./... # Lint Go code golangci-lint run # Format Go code go fmt ./... goimports -w . # Update go.mod of all services find . -type f -name "*.mod" -printf "%h\0" | sort -zu | xargs -0 -I{} sh -c 'cd "{}" && go mod tidy' # Run all backend tests go test $(go list -f '{{.Dir}}/...' -m | xargs) # Go vet all backend code go vet $(go list -f '{{.Dir}}/...' -m | xargs) # Lint all backend code golangci-lint run $(go list -f '{{.Dir}}/...' -m | xargs) ``` ### Development Environment ```bash # Start development stack with Docker Compose cd infrastructure/dev docker-compose up -d # This starts: # - PostgreSQL database (port 5432) # - Redis cache (port 6379) # - Zitadel auth server (port 8080) ``` ### Frontend Development Note: This project uses Yarn with `nodeLinker: node-modules`. PnP files like `.pnp.cjs` and `.pnp.loader.mjs` are not used. Status: `Web` commands are implemented. `Tauri cross-platform` commands are planned and require a future `frontend/apps/cross-platform` workspace. ```bash # Web application cd frontend yarn install yarn dev # Start web development server (SolidJS) yarn build # Build all frontend workspaces yarn test # Run Vitest suites yarn lint # ESLint yarn format:check # Prettier check # Cross-platform packaging with Tauri (desktop/mobile) cd frontend/apps/cross-platform yarn tauri dev # Run Tauri app with embedded web UI yarn tauri build # Build release bundles ```