diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md index a8cd0e4..6a32004 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.md @@ -1,30 +1,27 @@ --- -name: Documentation -about: Request documentation improvements or additions -labels: documentation +name: Documentation Issue +about: Report errors, unclear content, or missing documentation +labels: docs, triage --- -## Type - - -- [ ] Missing docs β€” feature/API not documented -- [ ] Incorrect docs β€” documentation is wrong or outdated -- [ ] Unclear docs β€” documentation exists but is confusing -- [ ] New guide β€” tutorial or how-to needed - ## Location - - + -## Summary +## Issue Type - + +- [ ] Typo or grammatical error +- [ ] Unclear or confusing explanation +- [ ] Missing information +- [ ] Outdated content +- [ ] Broken link +- [ ] Code example doesn't work -## Proposed Content +## Description - + -## References +## Suggested Fix - + diff --git a/.github/internal/plans/MASTER.md b/.github/internal/plans/MASTER.md deleted file mode 100644 index 51e7ad7..0000000 --- a/.github/internal/plans/MASTER.md +++ /dev/null @@ -1,184 +0,0 @@ -# modkit Documentation & Feature Plan - -## Overview - -This document tracks all planned improvements to modkit, organized by type and priority. -Each item links to its detailed implementation plan. - -## Status Legend - -- πŸ”΄ Not started -- 🟑 In progress -- 🟒 Complete -- ⏭️ Deferred (post-MVP) - ---- - -## Code Changes - -| # | Topic | Status | Plan | Priority | Summary | -|---|-------|--------|------|----------|---------| -| C1 | Controller Registry Scoping | 🟒 | [code/01-controller-registry-scoping.md](code/01-controller-registry-scoping.md) | Medium | Namespace controller keys to allow same name across modules | -| C2 | Router Group and Use | 🟒 | [code/02-router-group-use.md](code/02-router-group-use.md) | High | Add `Group()` and `Use()` to Router interface (docs describe but not implemented) | -| C3 | App Container Access | 🟒 | [code/03-app-container-access.md](code/03-app-container-access.md) | Medium | Fix docs to use `App.Get()` instead of unexported `app.Container` | -| C4 | Logger Interface Alignment | 🟒 | [code/04-logger-interface-alignment.md](code/04-logger-interface-alignment.md) | Medium | Align Logger interface with docs: `...any` args, add `Warn` method | -| C5 | NewSlogLogger Rename | 🟒 | [code/05-newsloglogger-rename.md](code/05-newsloglogger-rename.md) | Low | Rename `NewSlog` β†’ `NewSlogLogger` to match docs | -| C6 | Graceful Shutdown | 🟒 | [code/06-graceful-shutdown.md](code/06-graceful-shutdown.md) | Medium | Implement SIGINT/SIGTERM handling in `Serve()` (docs claim but not implemented) | - ---- - -## SDLC / Tooling - -| # | Topic | Status | Plan | Summary | -|---|-------|--------|------|---------| -| S1 | Commit Validation | 🟒 | [sdlc/01-commit-validation.md](sdlc/01-commit-validation.md) | Lefthook + Go commitlint | -| S2 | Changelog Automation | 🟒 | β€” | Auto-generate CHANGELOG.md from commits | -| S3 | Release Workflow | 🟒 | β€” | GitHub Actions with go-semantic-release | -| S4 | Pre-commit Hooks | 🟒 | β€” | Run fmt/lint before commit | -| S5 | Test Coverage | 🟒 | β€” | Coverage reporting in CI | - ---- - -## Documentation Improvements - -Ordered by logical implementation sequence. Complete earlier items before later ones. - -| # | Topic | Status | Plan | NestJS Equivalent | Approach | -|---|-------|--------|------|-------------------|----------| -| D1 | Introduction & Overview | 🟒 | [docs/01-intro-overview.md](docs/01-intro-overview.md) | Introduction, Overview, First Steps | Add "Why modkit", architecture flow, bootstrap snippet | -| D2 | Modules | 🟒 | [docs/02-modules.md](docs/02-modules.md) | Modules | Clarify pointer identity, Definition() purity | -| D3 | Providers | 🟒 | [docs/03-providers.md](docs/03-providers.md) | Providers | Document lazy singleton lifecycle, cycle errors | -| D4 | Controllers | 🟒 | [docs/04-controllers.md](docs/04-controllers.md) | Controllers | Document RouteRegistrar contract | -| D5 | Middleware | 🟒 | [docs/05-middleware.md](docs/05-middleware.md) | Middleware | New guide: Go http.Handler patterns | -| D6 | Error Handling | 🟒 | [docs/06-error-handling.md](docs/06-error-handling.md) | Exception Filters | New guide: handler errors + middleware | -| D7 | Validation | 🟒 | [docs/07-validation.md](docs/07-validation.md) | Pipes | New guide: explicit decode/validate | -| D8 | Auth & Guards | 🟒 | [docs/08-auth-guards.md](docs/08-auth-guards.md) | Guards | New guide: auth middleware + context | -| D9 | Interceptors | 🟒 | [docs/09-interceptors.md](docs/09-interceptors.md) | Interceptors | New guide: middleware wrappers | -| D10 | Context Helpers | 🟒 | [docs/10-context-helpers.md](docs/10-context-helpers.md) | Custom Decorators | New guide: typed context keys | - ---- - -## Post-MVP Roadmap - -| Topic | Plan | Summary | -|-------|------|---------| -| modkitx | [docs/99-post-mvp-roadmap.md](docs/99-post-mvp-roadmap.md) | Optional ergonomics layer (builders, helpers) | -| modkit-cli | [docs/99-post-mvp-roadmap.md](docs/99-post-mvp-roadmap.md) | Scaffolding tool (not runtime) | -| gRPC Adapter | [docs/99-post-mvp-roadmap.md](docs/99-post-mvp-roadmap.md) | Future adapter package | - ---- - -## NestJS Topics Intentionally Not Covered - -These are handled by Go-idiomatic patterns documented in guides, not framework abstractions: - -| NestJS Topic | modkit Approach | See Guide | -|--------------|-----------------|-----------| -| Exception Filters | Return errors + error middleware | D6: Error Handling | -| Pipes | Explicit json.Decode + validate | D7: Validation | -| Guards | Auth middleware | D8: Auth & Guards | -| Interceptors | Middleware wrappers | D9: Interceptors | -| Custom Decorators | Context helpers | D10: Context Helpers | -| Global Modules | Not supported (breaks explicit visibility) | β€” | -| Dynamic Modules | Options pattern in constructors | D2: Modules | - ---- - -## Implementation Notes - -1. **D1-D10 complete** β€” All documentation guides in `docs/guides/` are complete -2. **C1-C6 complete** β€” All code changes implemented and merged -3. **Testing** β€” Each guide references examples from `examples/hello-mysql` - ---- - -## Dependency Analysis - -### Story Dependencies - -| Story | Can Start Immediately | Blocks | Blocked By | -|-------|----------------------|--------|------------| -| C1 | βœ… | β€” | β€” | -| C2 | βœ… | D9 | β€” | -| C3 | βœ… | β€” | β€” | -| C4 | βœ… | C5 | β€” | -| C5 | ❌ | β€” | C4 | -| C6 | βœ… | β€” | β€” | -| D9 | ❌ | β€” | C2 | -| D10 | βœ… | β€” | β€” | - -### Sequential Dependencies - -```text -C4 (Logger Interface) ──► C5 (NewSlogLogger Rename) - β”‚ - └── Both modify modkit/logging/slog.go and logger_test.go - C5 must be done AFTER C4 to avoid conflicts - -C2 (Router Group/Use) ──► D9 (Interceptors Guide) - β”‚ - └── D9 documents middleware wrapper patterns using - Group() and Use() methods from C2 -``` - -### Parallel Work Groups - -**Group A: Kernel** β€” isolated from http/logging -- C1: Controller Registry Scoping - -**Group B: HTTP** β€” isolated from kernel/logging -- C2: Router Group and Use -- C6: Graceful Shutdown *(different files)* - -**Group C: Logging** β€” sequential internally -- C4: Logger Interface Alignment *(first)* -- C5: NewSlogLogger Rename *(after C4)* - -**Group D: Docs Only** β€” no code dependencies -- C3: App Container Access -- D10: Context Helpers - -**Group E: Depends on C2** -- D9: Interceptors - ---- - -## Execution Plan - -### Optimal Agent Assignment (4 concurrent agents) - -Sequential work stays within the same agent context β€” no cross-agent waiting required. - -| Agent | Work | Notes | -|-------|------|-------| -| Agent 1 | C1, C3 | Independent stories, parallel within agent | -| Agent 2 | C2 β†’ D9 | D9 continues immediately after C2 | -| Agent 3 | C4 β†’ C5 | C5 continues immediately after C4; same files | -| Agent 4 | C6, D10 | Independent stories, parallel within agent | - -```text -Agent 1: ─── C1 ───┬─── C3 ─── - β”‚ -Agent 2: ─── C2 ───────────────► D9 ─── - β”‚ -Agent 3: ─── C4 ───────────────► C5 ─── - β”‚ -Agent 4: ─── C6 ───┬─── D10 ─── -``` - -**Why this works:** -- Each agent runs to completion without waiting for other agents -- Sequential dependencies (C4β†’C5, C2β†’D9) stay within same agent context -- Agent has full knowledge of prior changes when continuing to dependent work -- Same files stay together (C4/C5 both touch `logging/slog.go`) - -### Priority Order (single agent) - -1. **C2 (Router Group/Use)** β€” High priority; unblocks D9 -2. **C4 (Logger Interface)** β€” Unblocks C5 -3. **C3 (Container Access)** β€” Docs fix only; quick win -4. **C6 (Graceful Shutdown)** β€” Docs promise feature that doesn't exist -5. **C1 (Controller Scoping)** β€” Improves multi-module apps -6. **D10 (Context Helpers)** β€” Standalone doc -7. **C5 (NewSlogLogger)** β€” Simple rename; requires C4 -8. **D9 (Interceptors)** β€” Requires C2 diff --git a/.github/internal/plans/code/01-controller-registry-scoping.md b/.github/internal/plans/code/01-controller-registry-scoping.md deleted file mode 100644 index 7365a1a..0000000 --- a/.github/internal/plans/code/01-controller-registry-scoping.md +++ /dev/null @@ -1,283 +0,0 @@ -# C1: Controller Registry Scoping - -**Status:** πŸ”΄ Not started -**Type:** Code change -**Priority:** Medium - ---- - -## Motivation - -Currently, controller names must be globally unique across all modules. If two modules each define a controller named `UsersController`, bootstrap fails with `DuplicateControllerNameError`. - -This is unnecessarily restrictive. In a modular architecture, different modules should be able to use the same controller names without conflict. For example: -- `admin` module with `UsersController` -- `api` module with `UsersController` - -The fix is to namespace controller registry keys by module name (e.g., `admin:UsersController`, `api:UsersController`), while still enforcing uniqueness within a single module. - ---- - -## Assumptions - -1. Controller name uniqueness only matters within a module, not globally -2. The HTTP adapter (`RegisterRoutes`) doesn't depend on specific controller key format -3. Changing the key format is a non-breaking change since `App.Controllers` is `map[string]any` -4. Existing code that accesses controllers by name may need updating (check examples) - ---- - -## Requirements - -### R1: Namespace controller keys - -Controller registry keys should be `moduleName:controllerName` instead of just `controllerName`. - -### R2: Enforce uniqueness per module only - -Duplicate controller names in the same module should still error. Duplicate names across different modules should be allowed. - -### R3: Update DuplicateControllerNameError - -Consider adding `Module` field to the error for better debugging. - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `modkit/kernel/bootstrap.go` | Namespace keys, per-module duplicate check | -| `modkit/kernel/errors.go` | Optionally add `Module` field to `DuplicateControllerNameError` | -| `modkit/kernel/bootstrap_test.go` | Add tests for cross-module same-name controllers | - ---- - -## Implementation - -### Step 1: Add helper function in bootstrap.go - -```go -func controllerKey(moduleName, controllerName string) string { - return moduleName + ":" + controllerName -} -``` - -### Step 2: Update controller registration loop - -Replace the current implementation in `Bootstrap()`: - -```go -// Current implementation -controllers := make(map[string]any) -for _, node := range graph.Modules { - resolver := container.resolverFor(node.Name) - for _, controller := range node.Def.Controllers { - if _, exists := controllers[controller.Name]; exists { - return nil, &DuplicateControllerNameError{Name: controller.Name} - } - instance, err := controller.Build(resolver) - if err != nil { - return nil, &ControllerBuildError{Module: node.Name, Controller: controller.Name, Err: err} - } - controllers[controller.Name] = instance - } -} -``` - -With: - -```go -controllers := make(map[string]any) -perModule := make(map[string]map[string]bool) - -for _, node := range graph.Modules { - if perModule[node.Name] == nil { - perModule[node.Name] = make(map[string]bool) - } - resolver := container.resolverFor(node.Name) - for _, controller := range node.Def.Controllers { - // Check for duplicates within the same module - if perModule[node.Name][controller.Name] { - return nil, &DuplicateControllerNameError{Module: node.Name, Name: controller.Name} - } - perModule[node.Name][controller.Name] = true - - instance, err := controller.Build(resolver) - if err != nil { - return nil, &ControllerBuildError{Module: node.Name, Controller: controller.Name, Err: err} - } - controllers[controllerKey(node.Name, controller.Name)] = instance - } -} -``` - -### Step 3: Update DuplicateControllerNameError (optional enhancement) - -In `errors.go`, add `Module` field: - -```go -type DuplicateControllerNameError struct { - Module string - Name string -} - -func (e *DuplicateControllerNameError) Error() string { - if e.Module != "" { - return fmt.Sprintf("duplicate controller name in module %q: %s", e.Module, e.Name) - } - return fmt.Sprintf("duplicate controller name: %s", e.Name) -} -``` - ---- - -## Validation - -### Unit Tests - -Add to `modkit/kernel/bootstrap_test.go`: - -```go -func TestBootstrapAllowsSameControllerNameAcrossModules(t *testing.T) { - // Module B has controller "Shared" - modB := &testModule{ - name: "B", - controllers: []module.ControllerDef{{ - Name: "Shared", - Build: func(r module.Resolver) (any, error) { - return "controller-from-B", nil - }, - }}, - } - - // Module A imports B and also has controller "Shared" - modA := &testModule{ - name: "A", - imports: []module.Module{modB}, - controllers: []module.ControllerDef{{ - Name: "Shared", - Build: func(r module.Resolver) (any, error) { - return "controller-from-A", nil - }, - }}, - } - - app, err := kernel.Bootstrap(modA) - if err != nil { - t.Fatalf("expected no error, got: %v", err) - } - - // Should have 2 controllers with namespaced keys - if len(app.Controllers) != 2 { - t.Fatalf("expected 2 controllers, got %d", len(app.Controllers)) - } - - // Verify both controllers are accessible - if _, ok := app.Controllers["A:Shared"]; !ok { - t.Error("expected controller 'A:Shared' not found") - } - if _, ok := app.Controllers["B:Shared"]; !ok { - t.Error("expected controller 'B:Shared' not found") - } - - // Verify correct instances - if app.Controllers["A:Shared"] != "controller-from-A" { - t.Error("controller A:Shared has wrong value") - } - if app.Controllers["B:Shared"] != "controller-from-B" { - t.Error("controller B:Shared has wrong value") - } -} - -func TestBootstrapRejectsDuplicateControllerInSameModule(t *testing.T) { - mod := &testModule{ - name: "test", - controllers: []module.ControllerDef{ - {Name: "Dup", Build: func(r module.Resolver) (any, error) { return "a", nil }}, - {Name: "Dup", Build: func(r module.Resolver) (any, error) { return "b", nil }}, - }, - } - - _, err := kernel.Bootstrap(mod) - if err == nil { - t.Fatal("expected error for duplicate controller name in same module") - } - - var dupErr *kernel.DuplicateControllerNameError - if !errors.As(err, &dupErr) { - t.Fatalf("expected DuplicateControllerNameError, got: %T", err) - } - if dupErr.Name != "Dup" { - t.Errorf("expected name 'Dup', got %q", dupErr.Name) - } -} - -func TestControllerKeyFormat(t *testing.T) { - mod := &testModule{ - name: "users", - controllers: []module.ControllerDef{{ - Name: "Controller", - Build: func(r module.Resolver) (any, error) { return nil, nil }, - }}, - } - - app, err := kernel.Bootstrap(mod) - if err != nil { - t.Fatal(err) - } - - // Key should be "module:controller" - if _, ok := app.Controllers["users:Controller"]; !ok { - t.Errorf("expected key 'users:Controller', got keys: %v", keys(app.Controllers)) - } -} - -func keys(m map[string]any) []string { - k := make([]string, 0, len(m)) - for key := range m { - k = append(k, key) - } - return k -} -``` - -### Integration Test - -Verify HTTP adapter still works: - -```bash -go test ./modkit/http/... -go test ./examples/... -``` - -### Check Examples - -Verify example apps don't access `app.Controllers` by raw name. If they do, update them. - ---- - -## Acceptance Criteria - -- [ ] Controller keys are namespaced as `moduleName:controllerName` -- [ ] Same controller name in different modules is allowed -- [ ] Same controller name in same module is rejected with `DuplicateControllerNameError` -- [ ] `DuplicateControllerNameError` includes module name in error message -- [ ] `RegisterRoutes` works correctly (iterates all controllers regardless of key format) -- [ ] All existing tests pass -- [ ] New tests cover: - - [ ] Cross-module same name allowed - - [ ] Same-module duplicate rejected - - [ ] Key format is correct -- [ ] Example apps work correctly -- [ ] `make lint` passes -- [ ] `make test` passes - ---- - -## References - -- Current implementation: `modkit/kernel/bootstrap.go` (lines 27-40) -- Error types: `modkit/kernel/errors.go` (lines 81-87) -- HTTP adapter: `modkit/http/router.go` (`RegisterRoutes` function) -- Existing bootstrap tests: `modkit/kernel/bootstrap_test.go` diff --git a/.github/internal/plans/code/02-router-group-use.md b/.github/internal/plans/code/02-router-group-use.md deleted file mode 100644 index a112acb..0000000 --- a/.github/internal/plans/code/02-router-group-use.md +++ /dev/null @@ -1,231 +0,0 @@ -# C2: Extend Router Interface with Group and Use - -**Status:** πŸ”΄ Not started -**Type:** Code change -**Priority:** High - ---- - -## Motivation - -The current `Router` interface in `modkit/http/router.go` only exposes `Handle(method, pattern, handler)`. However, the documentation describes a richer interface including `Group()` for route grouping and `Use()` for middleware attachment. - -Multiple guides depend on this API: -- `docs/guides/controllers.md` shows `r.Group("/users", ...)` patterns -- `docs/guides/middleware.md` shows `r.Use(authMiddleware)` patterns -- `docs/reference/api.md` documents the full interface - -Without this, users following the guides will encounter compilation errors. - ---- - -## Assumptions - -1. The underlying chi router already supports `Group` and `Use` β€” we're exposing existing functionality -2. The `routerAdapter` wrapper can be extended to delegate to chi -3. No breaking changes to existing code β€” we're adding methods, not changing existing ones - ---- - -## Requirements - -### R1: Add Group method to Router interface - -```go -Group(pattern string, fn func(Router)) -``` - -- Creates a sub-router scoped to the pattern prefix -- The callback receives a `Router` that can register routes relative to the group -- Middleware applied via `Use` inside the group only affects routes in that group - -### R2: Add Use method to Router interface - -```go -Use(middlewares ...func(http.Handler) http.Handler) -``` - -- Attaches middleware to the router (or group) -- Middleware executes in order of registration -- Must work at both global level and within groups - -### R3: Update routerAdapter to implement new methods - -The `routerAdapter` struct must delegate to the underlying `chi.Router` methods. - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `modkit/http/router.go` | Add `Group` and `Use` to interface, implement in adapter | -| `modkit/http/router_test.go` | Add tests for new functionality | - ---- - -## Implementation - -### Step 1: Update Router interface - -In `modkit/http/router.go`, change: - -```go -type Router interface { - Handle(method string, pattern string, handler http.Handler) -} -``` - -To: - -```go -type Router interface { - Handle(method string, pattern string, handler http.Handler) - Group(pattern string, fn func(Router)) - Use(middlewares ...func(http.Handler) http.Handler) -} -``` - -### Step 2: Implement Group in routerAdapter - -```go -func (r routerAdapter) Group(pattern string, fn func(Router)) { - r.Router.Route(pattern, func(sub chi.Router) { - fn(routerAdapter{Router: sub}) - }) -} -``` - -### Step 3: Implement Use in routerAdapter - -```go -func (r routerAdapter) Use(middlewares ...func(http.Handler) http.Handler) { - r.Router.Use(middlewares...) -} -``` - ---- - -## Validation - -### Unit Tests - -Add to `modkit/http/router_test.go`: - -```go -func TestRouterGroup(t *testing.T) { - router := chi.NewRouter() - r := AsRouter(router) - - called := false - r.Group("/api", func(sub Router) { - sub.Handle(http.MethodGet, "/users", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - called = true - })) - }) - - req := httptest.NewRequest(http.MethodGet, "/api/users", nil) - rec := httptest.NewRecorder() - router.ServeHTTP(rec, req) - - if !called { - t.Error("handler not called for grouped route") - } -} - -func TestRouterUse(t *testing.T) { - router := chi.NewRouter() - r := AsRouter(router) - - middlewareCalled := false - r.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - middlewareCalled = true - next.ServeHTTP(w, req) - }) - }) - - r.Handle(http.MethodGet, "/test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) - - req := httptest.NewRequest(http.MethodGet, "/test", nil) - rec := httptest.NewRecorder() - router.ServeHTTP(rec, req) - - if !middlewareCalled { - t.Error("middleware not called") - } -} - -func TestRouterGroupWithMiddleware(t *testing.T) { - router := chi.NewRouter() - r := AsRouter(router) - - groupMiddlewareCalled := false - globalHandlerCalled := false - groupHandlerCalled := false - - r.Handle(http.MethodGet, "/public", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - globalHandlerCalled = true - })) - - r.Group("/protected", func(sub Router) { - sub.Use(func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - groupMiddlewareCalled = true - next.ServeHTTP(w, req) - }) - }) - sub.Handle(http.MethodGet, "/resource", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - groupHandlerCalled = true - })) - }) - - // Call public route - middleware should NOT be called - req1 := httptest.NewRequest(http.MethodGet, "/public", nil) - router.ServeHTTP(httptest.NewRecorder(), req1) - - if groupMiddlewareCalled { - t.Error("group middleware should not affect routes outside group") - } - - // Call protected route - middleware SHOULD be called - req2 := httptest.NewRequest(http.MethodGet, "/protected/resource", nil) - router.ServeHTTP(httptest.NewRecorder(), req2) - - if !groupMiddlewareCalled || !groupHandlerCalled { - t.Error("group middleware or handler not called") - } -} -``` - -### Integration Test - -Verify the example app still works: - -```bash -go test ./modkit/http/... -go test ./examples/hello-mysql/... -``` - ---- - -## Acceptance Criteria - -- [ ] `Router` interface includes `Group(pattern string, fn func(Router))` -- [ ] `Router` interface includes `Use(middlewares ...func(http.Handler) http.Handler)` -- [ ] `routerAdapter` implements both methods by delegating to chi -- [ ] Routes registered inside `Group` are prefixed correctly -- [ ] Middleware applied via `Use` inside a group only affects that group -- [ ] All existing tests pass -- [ ] New unit tests cover `Group`, `Use`, and combination scenarios -- [ ] `make lint` passes -- [ ] `make test` passes - ---- - -## References - -- Current implementation: `modkit/http/router.go` -- Documented interface: `docs/reference/api.md` (lines 174-179) -- Guide usage: `docs/guides/controllers.md` (lines 126-134), `docs/guides/middleware.md` (lines 44-57) -- chi documentation: https://github.com/go-chi/chi diff --git a/.github/internal/plans/code/03-app-container-access.md b/.github/internal/plans/code/03-app-container-access.md deleted file mode 100644 index 7fab927..0000000 --- a/.github/internal/plans/code/03-app-container-access.md +++ /dev/null @@ -1,176 +0,0 @@ -# C3: Standardize Container Access Pattern - -**Status:** πŸ”΄ Not started -**Type:** Code/Docs alignment -**Priority:** Medium - ---- - -## Motivation - -Documentation shows users accessing providers via `app.Container.Get("token")`, but `container` is an unexported field in the `App` struct. This causes confusion when users follow the guides. - -The `App` struct already has `App.Get(token)` and `App.Resolver()` methods that provide the same functionality, but docs don't use them consistently. - -**Options:** -1. **Export Container** β€” Change `container` to `Container` -2. **Fix docs** β€” Update all docs to use `App.Get(token)` instead - -This plan recommends **Option 2** (fix docs) because: -- `App.Get()` is cleaner API β€” single method vs nested access -- Keeps internal container implementation private -- No breaking changes if container internals change later - ---- - -## Assumptions - -1. `App.Get(token)` provides equivalent functionality to direct container access -2. All doc references to `app.Container.Get()` can be replaced with `app.Get()` -3. No external code depends on `Container` being exported (it never was) - ---- - -## Requirements - -### R1: Audit all documentation references - -Find and update all instances of `app.Container.Get()` pattern. - -### R2: Ensure App.Get() is documented - -The `App.Get(token)` method should be documented in the API reference. - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `docs/guides/providers.md` | Update container access pattern | -| `docs/guides/middleware.md` | Update container access pattern | -| `docs/guides/comparison.md` | Update container access pattern | -| `docs/guides/authentication.md` | Update container access pattern | -| `docs/reference/api.md` | Ensure `App.Get()` is documented, remove `Container` field if shown | - ---- - -## Implementation - -### Step 1: Update docs/guides/providers.md - -Change (around line 202): - -```go -// Cleanup on shutdown -if db, err := app.Container.Get("db.connection"); err == nil { - db.(*sql.DB).Close() -} -``` - -To: - -```go -// Cleanup on shutdown -if db, err := app.Get("db.connection"); err == nil { - db.(*sql.DB).Close() -} -``` - -### Step 2: Update docs/guides/middleware.md - -Change (around line 289): - -```go -authMW, _ := app.Container.Get("middleware.auth") -``` - -To: - -```go -authMW, _ := app.Get("middleware.auth") -``` - -### Step 3: Update docs/guides/comparison.md - -Change (around line 130): - -```go -svc, _ := app.Container.Get("users.service") -``` - -To: - -```go -svc, _ := app.Get("users.service") -``` - -### Step 4: Update docs/guides/authentication.md - -Change (around line 225): - -```go -authMW, _ := app.Container.Get("auth.middleware") -``` - -To: - -```go -authMW, _ := app.Get("auth.middleware") -``` - -### Step 5: Update docs/reference/api.md - -Ensure `App` struct documentation shows: - -```go -type App struct { - Controllers map[string]any - Graph *Graph -} - -// Get resolves a token from the root module scope. -func (a *App) Get(token Token) (any, error) - -// Resolver returns a root-scoped resolver that enforces module visibility. -func (a *App) Resolver() Resolver -``` - -Remove any reference to `Container` field if present. - ---- - -## Validation - -### Grep Verification - -After changes, this should return no results: - -```bash -grep -r "app\.Container" docs/ -``` - -### Build Check - -Ensure example apps compile (they may use the correct pattern already): - -```bash -go build ./examples/... -``` - ---- - -## Acceptance Criteria - -- [ ] No documentation references `app.Container.Get()` -- [ ] All container access uses `app.Get(token)` pattern -- [ ] `App.Get()` method is documented in API reference -- [ ] Example apps compile and work correctly -- [ ] `grep -r "app\.Container" docs/` returns empty - ---- - -## References - -- Current App implementation: `modkit/kernel/bootstrap.go` (lines 5-9, 49-57) -- Affected docs: `docs/guides/providers.md`, `docs/guides/middleware.md`, `docs/guides/comparison.md`, `docs/guides/authentication.md` diff --git a/.github/internal/plans/code/04-logger-interface-alignment.md b/.github/internal/plans/code/04-logger-interface-alignment.md deleted file mode 100644 index 7f8b6c8..0000000 --- a/.github/internal/plans/code/04-logger-interface-alignment.md +++ /dev/null @@ -1,240 +0,0 @@ -# C4: Align Logger Interface with Documentation - -**Status:** πŸ”΄ Not started -**Type:** Code change -**Priority:** Medium - ---- - -## Motivation - -The `logging.Logger` interface implementation doesn't match documentation: - -| Aspect | Documented (api.md) | Actual (logger.go) | -|--------|---------------------|-------------------| -| Method signature | `Debug(msg string, args ...any)` | `Debug(msg string, attrs ...slog.Attr)` | -| Warn method | `Warn(msg string, args ...any)` | **Missing** | -| With signature | `With(args ...any) Logger` | `With(attrs ...slog.Attr) Logger` | - -The documented `...any` signature is more ergonomic and matches `slog.Logger` patterns. The current `...slog.Attr` signature is more restrictive and less user-friendly. - ---- - -## Assumptions - -1. Changing the interface is acceptable since the project is pre-v0.1.0 -2. The `...any` pattern aligns better with slog's variadic key-value approach -3. Adding `Warn` method provides feature parity with standard log levels - ---- - -## Requirements - -### R1: Update Logger interface to use ...any - -Change method signatures to accept variadic `any` like slog does. - -### R2: Add Warn method - -Include `Warn(msg string, args ...any)` for feature parity. - -### R3: Update slog adapter implementation - -The `slogAdapter` must convert `...any` args to slog calls. - -### R4: Update nop logger - -The `nopLogger` must implement the new interface. - -### R5: Update RequestLogger middleware - -If it uses the Logger interface, ensure it works with new signature. - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `modkit/logging/logger.go` | Update interface definition | -| `modkit/logging/slog.go` | Update adapter implementation | -| `modkit/logging/nop.go` | Update nop implementation | -| `modkit/logging/logger_test.go` | Update/add tests | -| `modkit/http/logging.go` | Verify compatibility | - ---- - -## Implementation - -### Step 1: Update logger.go - -Change: - -```go -type Logger interface { - Debug(msg string, attrs ...slog.Attr) - Info(msg string, attrs ...slog.Attr) - Error(msg string, attrs ...slog.Attr) - With(attrs ...slog.Attr) Logger -} -``` - -To: - -```go -type Logger interface { - Debug(msg string, args ...any) - Info(msg string, args ...any) - Warn(msg string, args ...any) - Error(msg string, args ...any) - With(args ...any) Logger -} -``` - -### Step 2: Update slog.go - -Change adapter to pass args directly to slog (which accepts `...any`): - -```go -type slogAdapter struct { - logger *slog.Logger -} - -func NewSlog(logger *slog.Logger) Logger { - if logger == nil { - return Nop() - } - return slogAdapter{logger: logger} -} - -func (s slogAdapter) Debug(msg string, args ...any) { - s.logger.Debug(msg, args...) -} - -func (s slogAdapter) Info(msg string, args ...any) { - s.logger.Info(msg, args...) -} - -func (s slogAdapter) Warn(msg string, args ...any) { - s.logger.Warn(msg, args...) -} - -func (s slogAdapter) Error(msg string, args ...any) { - s.logger.Error(msg, args...) -} - -func (s slogAdapter) With(args ...any) Logger { - return slogAdapter{logger: s.logger.With(args...)} -} -``` - -Remove the `attrsToAny` helper function as it's no longer needed. - -### Step 3: Update nop.go - -```go -type nopLogger struct{} - -func Nop() Logger { - return nopLogger{} -} - -func (nopLogger) Debug(string, ...any) {} -func (nopLogger) Info(string, ...any) {} -func (nopLogger) Warn(string, ...any) {} -func (nopLogger) Error(string, ...any) {} -func (nopLogger) With(...any) Logger { return nopLogger{} } -``` - -### Step 4: Update http/logging.go - -Check if `RequestLogger` uses `slog.Attr` directly. If so, update to use key-value pairs: - -```go -logger.Info("http request", - "method", r.Method, - "path", r.URL.Path, - "status", ww.Status(), - "duration", duration, -) -``` - ---- - -## Validation - -### Unit Tests - -Add/update tests in `modkit/logging/logger_test.go`: - -```go -func TestSlogAdapter(t *testing.T) { - var buf bytes.Buffer - handler := slog.NewTextHandler(&buf, nil) - logger := NewSlog(slog.New(handler)) - - logger.Info("test message", "key", "value", "count", 42) - - output := buf.String() - if !strings.Contains(output, "test message") { - t.Error("message not logged") - } - if !strings.Contains(output, "key=value") { - t.Error("key-value not logged") - } -} - -func TestLoggerWarn(t *testing.T) { - var buf bytes.Buffer - handler := slog.NewTextHandler(&buf, nil) - logger := NewSlog(slog.New(handler)) - - logger.Warn("warning message") - - if !strings.Contains(buf.String(), "WARN") { - t.Error("warn level not used") - } -} - -func TestNopLogger(t *testing.T) { - logger := Nop() - - // Should not panic - logger.Debug("msg") - logger.Info("msg") - logger.Warn("msg") - logger.Error("msg") - logger.With("key", "value").Info("msg") -} -``` - -### Integration Test - -```bash -go test ./modkit/logging/... -go test ./modkit/http/... -go test ./examples/... -``` - ---- - -## Acceptance Criteria - -- [ ] `Logger` interface uses `...any` for all methods -- [ ] `Logger` interface includes `Warn(msg string, args ...any)` -- [ ] `slogAdapter` implements new interface correctly -- [ ] `nopLogger` implements new interface correctly -- [ ] `RequestLogger` middleware works with new interface -- [ ] All existing tests pass -- [ ] New tests cover `Warn` method and key-value logging -- [ ] `make lint` passes -- [ ] `make test` passes -- [ ] Interface matches `docs/reference/api.md` documentation - ---- - -## References - -- Current implementation: `modkit/logging/logger.go`, `modkit/logging/slog.go`, `modkit/logging/nop.go` -- Documentation: `docs/reference/api.md` (lines 204-214) -- slog documentation: https://pkg.go.dev/log/slog diff --git a/.github/internal/plans/code/05-newsloglogger-rename.md b/.github/internal/plans/code/05-newsloglogger-rename.md deleted file mode 100644 index 3350abb..0000000 --- a/.github/internal/plans/code/05-newsloglogger-rename.md +++ /dev/null @@ -1,114 +0,0 @@ -# C5: Rename NewSlog to NewSlogLogger - -**Status:** πŸ”΄ Not started -**Type:** Code change -**Priority:** Low - ---- - -## Motivation - -Documentation refers to `logging.NewSlogLogger(slog.Default())` but the actual function is named `logging.NewSlog()`. This causes confusion when users copy code from docs. - -The longer name `NewSlogLogger` is clearer about what it returns (a `Logger` that wraps slog). - ---- - -## Assumptions - -1. No external code depends on `NewSlog` (project is pre-v0.1.0) -2. The rename is straightforward with no behavioral changes -3. Example apps may need updating if they use this function - ---- - -## Requirements - -### R1: Rename function - -Change `NewSlog` to `NewSlogLogger` in implementation. - -### R2: Update all usages - -Find and update any internal usages of the function. - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `modkit/logging/slog.go` | Rename `NewSlog` β†’ `NewSlogLogger` | -| `modkit/logging/logger_test.go` | Update test calls | -| Any example files using `NewSlog` | Update calls | - ---- - -## Implementation - -### Step 1: Rename in slog.go - -Change: - -```go -func NewSlog(logger *slog.Logger) Logger { -``` - -To: - -```go -func NewSlogLogger(logger *slog.Logger) Logger { -``` - -### Step 2: Find and update usages - -Search for usages: - -```bash -grep -r "NewSlog" --include="*.go" . -``` - -Update each occurrence from `NewSlog(` to `NewSlogLogger(`. - ---- - -## Validation - -### Compile Check - -```bash -go build ./... -``` - -### Test - -```bash -go test ./modkit/logging/... -go test ./examples/... -``` - -### Grep Verification - -After changes, only `NewSlogLogger` should exist: - -```bash -grep -r "NewSlog[^L]" --include="*.go" . -# Should return empty -``` - ---- - -## Acceptance Criteria - -- [ ] Function is named `NewSlogLogger` -- [ ] All usages updated -- [ ] No references to old `NewSlog` name remain (except in git history) -- [ ] All tests pass -- [ ] Function name matches `docs/reference/api.md` documentation - ---- - -## References - -- Current implementation: `modkit/logging/slog.go` (line 9) -- Documentation: `docs/reference/api.md` (lines 218-221) diff --git a/.github/internal/plans/code/06-graceful-shutdown.md b/.github/internal/plans/code/06-graceful-shutdown.md deleted file mode 100644 index c25e10c..0000000 --- a/.github/internal/plans/code/06-graceful-shutdown.md +++ /dev/null @@ -1,278 +0,0 @@ -# C6: Add Graceful Shutdown to Serve - -**Status:** πŸ”΄ Not started -**Type:** Code change -**Priority:** Medium - ---- - -## Motivation - -Documentation states that `mkhttp.Serve` handles SIGINT/SIGTERM automatically for graceful shutdown: - -> `mkhttp.Serve` handles SIGINT/SIGTERM automatically. -> β€” docs/faq.md - -However, the actual implementation is simply: - -```go -func Serve(addr string, handler http.Handler) error { - return listenAndServe(addr, handler) -} -``` - -This is just a passthrough to `http.ListenAndServe` with no signal handling. - ---- - -## Assumptions - -1. Graceful shutdown should wait for in-flight requests to complete -2. A reasonable default timeout (e.g., 30 seconds) is acceptable -3. The function signature can remain the same (no config options needed for MVP) -4. SIGINT and SIGTERM should both trigger shutdown - ---- - -## Requirements - -### R1: Handle SIGINT and SIGTERM - -The server should listen for these signals and initiate shutdown. - -### R2: Graceful shutdown with timeout - -Use `http.Server.Shutdown()` to allow in-flight requests to complete, with a timeout to prevent indefinite waiting. - -### R3: Return appropriate errors - -- Return `nil` on clean shutdown via signal -- Return error if server fails to start -- Return error if shutdown times out - -### R4: Maintain testability - -Keep the existing test hook (`listenAndServe` variable) or add new hooks for testing shutdown behavior. - ---- - -## Files to Modify - -| File | Change | -|------|--------| -| `modkit/http/server.go` | Implement graceful shutdown | -| `modkit/http/server_test.go` | Add tests for shutdown behavior | - ---- - -## Implementation - -### Step 1: Update server.go - -Replace the simple passthrough with proper shutdown handling: - -```go -package http - -import ( - "context" - "net/http" - "os" - "os/signal" - "syscall" - "time" -) - -// ShutdownTimeout is the maximum time to wait for in-flight requests during shutdown. -var ShutdownTimeout = 30 * time.Second - -// Serve starts an HTTP server on the given address and blocks until shutdown. -// It handles SIGINT and SIGTERM for graceful shutdown, waiting for in-flight -// requests to complete before returning. -func Serve(addr string, handler http.Handler) error { - server := &http.Server{ - Addr: addr, - Handler: handler, - } - - // Channel to receive server errors - errCh := make(chan error, 1) - go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errCh <- err - } - close(errCh) - }() - - // Wait for interrupt signal - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - select { - case err := <-errCh: - // Server failed to start - return err - case <-sigCh: - // Received shutdown signal - } - - // Graceful shutdown with timeout - ctx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) - defer cancel() - - if err := server.Shutdown(ctx); err != nil { - return err - } - - // Check if there was a server error during shutdown - if err := <-errCh; err != nil { - return err - } - - return nil -} -``` - -### Step 2: Add ServeWithContext for advanced use cases (optional) - -For users who need more control: - -```go -// ServeWithContext starts an HTTP server that shuts down when the context is canceled. -func ServeWithContext(ctx context.Context, addr string, handler http.Handler) error { - server := &http.Server{ - Addr: addr, - Handler: handler, - } - - errCh := make(chan error, 1) - go func() { - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { - errCh <- err - } - close(errCh) - }() - - select { - case err := <-errCh: - return err - case <-ctx.Done(): - } - - shutdownCtx, cancel := context.WithTimeout(context.Background(), ShutdownTimeout) - defer cancel() - - if err := server.Shutdown(shutdownCtx); err != nil { - return err - } - - if err := <-errCh; err != nil { - return err - } - - return nil -} -``` - ---- - -## Validation - -### Unit Tests - -Add to `modkit/http/server_test.go`: - -```go -func TestServeGracefulShutdown(t *testing.T) { - if testing.Short() { - t.Skip("skipping in short mode") - } - - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - // Start server in goroutine - errCh := make(chan error, 1) - go func() { - errCh <- Serve(":0", handler) // :0 picks random available port - }() - - // Give server time to start - time.Sleep(100 * time.Millisecond) - - // Send SIGINT to self - p, _ := os.FindProcess(os.Getpid()) - p.Signal(syscall.SIGINT) - - // Should shut down cleanly - select { - case err := <-errCh: - if err != nil { - t.Errorf("expected nil error on clean shutdown, got: %v", err) - } - case <-time.After(5 * time.Second): - t.Error("shutdown timed out") - } -} - -func TestServeWithContextCancellation(t *testing.T) { - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusOK) - }) - - ctx, cancel := context.WithCancel(context.Background()) - - errCh := make(chan error, 1) - go func() { - errCh <- ServeWithContext(ctx, ":0", handler) - }() - - // Give server time to start - time.Sleep(100 * time.Millisecond) - - // Cancel context - cancel() - - // Should shut down cleanly - select { - case err := <-errCh: - if err != nil { - t.Errorf("expected nil error on context cancel, got: %v", err) - } - case <-time.After(5 * time.Second): - t.Error("shutdown timed out") - } -} -``` - -### Manual Testing - -1. Start an example app -2. Send requests -3. Send SIGINT (Ctrl+C) -4. Verify clean shutdown message -5. Verify in-flight requests complete - ---- - -## Acceptance Criteria - -- [ ] `Serve` handles SIGINT signal -- [ ] `Serve` handles SIGTERM signal -- [ ] In-flight requests are allowed to complete (up to timeout) -- [ ] Returns `nil` on clean shutdown -- [ ] Returns error if server fails to start -- [ ] `ShutdownTimeout` is configurable via package variable -- [ ] All existing tests pass -- [ ] New tests verify shutdown behavior -- [ ] `make lint` passes -- [ ] `make test` passes - ---- - -## References - -- Current implementation: `modkit/http/server.go` -- Documentation claim: `docs/faq.md` (lines 186-197) -- Go http.Server.Shutdown: https://pkg.go.dev/net/http#Server.Shutdown diff --git a/.github/internal/plans/docs/01-intro-overview.md b/.github/internal/plans/docs/01-intro-overview.md deleted file mode 100644 index 29df225..0000000 --- a/.github/internal/plans/docs/01-intro-overview.md +++ /dev/null @@ -1,81 +0,0 @@ -# D1: Introduction & Overview - -**Status:** πŸ”΄ Not started -**Type:** Documentation improvement -**NestJS Equivalent:** Introduction, Overview, First Steps - ---- - -## Goal - -Clarify modkit's purpose and onboarding by adding a "Why modkit / no reflection" callout, a simple architecture flow, and a minimal bootstrap snippet. - -## Files to Modify - -- `README.md` -- `docs/guides/getting-started.md` - ---- - -## Task 1: Add "Why modkit" and architecture flow to README - -**Files:** -- Modify: `README.md` - -### Step 1: Add a short "Why modkit" section - -Suggested content: - -```markdown -## Why modkit? - -modkit is a Go‑idiomatic alternative to decorator‑driven frameworks. It keeps wiring explicit, avoids reflection, and makes module boundaries and dependencies visible in code. -``` - -### Step 2: Add an architecture flow callout - -Suggested content: - -```markdown -## Architecture Flow - -Module definitions β†’ kernel graph/visibility β†’ provider container β†’ controller instances β†’ HTTP adapter -``` - -### Step 3: Commit - -```bash -git add README.md -git commit -m "docs: clarify modkit purpose and architecture flow" -``` - ---- - -## Task 2: Add a minimal bootstrap snippet to getting started - -**Files:** -- Modify: `docs/guides/getting-started.md` - -### Step 1: Add a short "Minimal main.go" snippet near the top - -Suggested content: - -```go -func main() { - appInstance, err := kernel.Bootstrap(&app.AppModule{}) - if err != nil { - log.Fatal(err) - } - - router := mkhttp.NewRouter() - _ = mkhttp.RegisterRoutes(mkhttp.AsRouter(router), appInstance.Controllers) - _ = mkhttp.Serve(":8080", router) -} -``` - -### Step 2: Commit - -```bash -git add docs/guides/getting-started.md -git commit -m "docs: add minimal bootstrap snippet" -``` diff --git a/.github/internal/plans/docs/02-modules.md b/.github/internal/plans/docs/02-modules.md deleted file mode 100644 index 3921e17..0000000 --- a/.github/internal/plans/docs/02-modules.md +++ /dev/null @@ -1,82 +0,0 @@ -# D2: Modules - -**Status:** πŸ”΄ Not started -**Type:** Documentation improvement -**NestJS Equivalent:** Modules - ---- - -## Goal - -Clarify module semantics: -1. Modules must be pointers to ensure stable identity across shared imports -2. `Definition()` must be deterministic and side-effect free - -## Files to Modify - -- `docs/design/mvp.md` -- `docs/guides/modules.md` - ---- - -## Task 1: Add module identity section to design doc - -**Files:** -- Modify: `docs/design/mvp.md` - -### Step 1: Add a module identity section - -Suggested content: - -```markdown -**Module identity and pointers** - -Modules must be passed as pointers to ensure stable identity across shared imports. If two different module instances share the same `Name`, bootstrap will fail with a duplicate module name error. Reuse the same module pointer when importing a shared module. -``` - -### Step 2: Add Definition() purity section - -Suggested content: - -```markdown -**Definition() must be deterministic** - -`Definition()` can be called more than once during graph construction. It must be side-effect free and return consistent metadata for the lifetime of the module instance. -``` - -### Step 3: Commit - -```bash -git add docs/design/mvp.md -git commit -m "docs: clarify module identity and Definition purity" -``` - ---- - -## Task 2: Update modules guide - -**Files:** -- Modify: `docs/guides/modules.md` - -### Step 1: Add a note near Imports - -Suggested content: - -```markdown -**Module identity:** Always pass module pointers and reuse the same instance when sharing imports. Duplicate module names across different instances are errors. -``` - -### Step 2: Add a note near ModuleDef description - -Suggested content: - -```markdown -**Note:** `Definition()` must be deterministic and side-effect free. The kernel may call it multiple times when building the module graph. -``` - -### Step 3: Commit - -```bash -git add docs/guides/modules.md -git commit -m "docs: add module identity and Definition purity notes" -``` diff --git a/.github/internal/plans/docs/03-providers.md b/.github/internal/plans/docs/03-providers.md deleted file mode 100644 index bde45f7..0000000 --- a/.github/internal/plans/docs/03-providers.md +++ /dev/null @@ -1,62 +0,0 @@ -# D3: Providers - -**Status:** πŸ”΄ Not started -**Type:** Documentation improvement -**NestJS Equivalent:** Providers - ---- - -## Goal - -Document provider lifecycle: lazy singleton construction, visibility enforcement, and cycle/build error handling. - -## Files to Modify - -- `docs/design/mvp.md` -- `docs/guides/modules.md` - ---- - -## Task 1: Add provider lifecycle note to design doc - -**Files:** -- Modify: `docs/design/mvp.md` - -### Step 1: Add a short subsection under provider semantics - -Suggested content: - -```markdown -**Provider lifecycle** - -Providers are singletons built lazily on first `Get`. Cycles result in `ProviderCycleError`. Build errors surface as `ProviderBuildError` with module/token context. -``` - -### Step 2: Commit - -```bash -git add docs/design/mvp.md -git commit -m "docs: describe provider lifecycle and errors" -``` - ---- - -## Task 2: Add provider lifecycle note to modules guide - -**Files:** -- Modify: `docs/guides/modules.md` - -### Step 1: Add a short note near Providers section - -Suggested content: - -```markdown -Providers are lazy singletons; they are constructed on first `Get` and cached. Cycles are detected and returned as errors. -``` - -### Step 2: Commit - -```bash -git add docs/guides/modules.md -git commit -m "docs: add provider lifecycle note" -``` diff --git a/.github/internal/plans/docs/04-controllers.md b/.github/internal/plans/docs/04-controllers.md deleted file mode 100644 index 0e1e5c0..0000000 --- a/.github/internal/plans/docs/04-controllers.md +++ /dev/null @@ -1,70 +0,0 @@ -# D4: Controllers - -**Status:** πŸ”΄ Not started -**Type:** Documentation improvement -**NestJS Equivalent:** Controllers - ---- - -## Goal - -Explicitly document the controller contract: controllers must implement `http.RouteRegistrar` and will be registered via `http.RegisterRoutes`. - -## Files to Modify - -- `docs/design/http-adapter.md` -- `docs/guides/getting-started.md` - ---- - -## Task 1: Update HTTP adapter doc - -**Files:** -- Modify: `docs/design/http-adapter.md` - -### Step 1: Add a contract section - -Suggested content: - -```markdown -**Controller contract** - -Controllers must implement: - -```go -type RouteRegistrar interface { - RegisterRoutes(router Router) -} -``` - -The HTTP adapter will type-assert each controller to `RouteRegistrar` and return an error if any controller does not implement it. -``` - -### Step 2: Commit - -```bash -git add docs/design/http-adapter.md -git commit -m "docs: clarify controller registration contract" -``` - ---- - -## Task 2: Update getting started guide - -**Files:** -- Modify: `docs/guides/getting-started.md` - -### Step 1: Add a minimal contract callout - -Suggested content: - -```markdown -Controllers must implement `modkit/http.RouteRegistrar` and will be registered by `RegisterRoutes`. -``` - -### Step 2: Commit - -```bash -git add docs/guides/getting-started.md -git commit -m "docs: note controller RouteRegistrar contract" -``` diff --git a/.github/internal/plans/docs/05-middleware.md b/.github/internal/plans/docs/05-middleware.md deleted file mode 100644 index 4088710..0000000 --- a/.github/internal/plans/docs/05-middleware.md +++ /dev/null @@ -1,105 +0,0 @@ -# D5: Middleware Guide - -**Status:** πŸ”΄ Not started -**Type:** New guide -**NestJS Equivalent:** Middleware - ---- - -## Goal - -Add a Go‑idiomatic middleware guide for modkit using `http.Handler` and `chi`. - -## Files to Create/Modify - -- Create: `docs/guides/middleware.md` -- Modify: `README.md` (add link) - ---- - -## Task 1: Create middleware guide - -**Files:** -- Create: `docs/guides/middleware.md` - -### Step 1: Draft the guide - -Include: - -1. **What middleware is in Go** β€” `func(http.Handler) http.Handler` -2. **Example using modkit/http.NewRouter() and RequestLogger** -3. **Ordering guidance** β€” recover β†’ auth β†’ logging -4. **Chi-specific patterns** β€” `router.Use()`, route groups - -Suggested structure: - -```markdown -# Middleware - -Middleware in Go wraps HTTP handlers to add cross-cutting behavior like logging, authentication, and error recovery. - -## The Middleware Pattern - -```go -func MyMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // before - next.ServeHTTP(w, r) - // after - }) -} -``` - -## Using Middleware with modkit - -```go -router := mkhttp.NewRouter() -router.Use(mkhttp.RequestLogger(logger)) -router.Use(RecoverMiddleware) -``` - -## Ordering - -Apply middleware in this order: -1. Recovery (outermost) -2. Request ID -3. Logging -4. Authentication -5. Route handlers (innermost) - -## Route-Specific Middleware - -Use chi route groups for middleware that applies to specific routes: - -```go -router.Route("/admin", func(r chi.Router) { - r.Use(RequireAdmin) - r.Get("/", adminHandler) -}) -``` -``` - -### Step 2: Commit - -```bash -git add docs/guides/middleware.md -git commit -m "docs: add middleware guide" -``` - ---- - -## Task 2: Link guide from README - -**Files:** -- Modify: `README.md` - -### Step 1: Add middleware guide to the Guides list - -Add `docs/guides/middleware.md` to the Guides section. - -### Step 2: Commit - -```bash -git add README.md -git commit -m "docs: link middleware guide" -``` diff --git a/.github/internal/plans/docs/06-error-handling.md b/.github/internal/plans/docs/06-error-handling.md deleted file mode 100644 index f90fc9b..0000000 --- a/.github/internal/plans/docs/06-error-handling.md +++ /dev/null @@ -1,117 +0,0 @@ -# D6: Error Handling Guide - -**Status:** πŸ”΄ Not started -**Type:** New guide -**NestJS Equivalent:** Exception Filters - ---- - -## Goal - -Document Go‑idiomatic error handling patterns (handler‑level errors and middleware) as the modkit equivalent of Nest exception filters. - -## Why Different from NestJS - -NestJS exception filters catch thrown exceptions and transform them. In Go, errors are returned values, not exceptions. The idiomatic approach is: -- Return errors from handlers -- Handle errors at the call site or via middleware -- Use structured error responses (RFC 7807 Problem Details) - -## Files to Create/Modify - -- Create: `docs/guides/error-handling.md` -- Modify: `README.md` (add link) - ---- - -## Task 1: Create error handling guide - -**Files:** -- Create: `docs/guides/error-handling.md` - -### Step 1: Draft the guide - -Include: - -1. **Handler-level error handling** β€” explicit `if err != nil` patterns -2. **Example using `httpapi.WriteProblem`** from hello-mysql -3. **Error middleware pattern** β€” centralized error mapping -4. **Structured errors** β€” RFC 7807 Problem Details - -Suggested structure: - -```markdown -# Error Handling - -Go handles errors as returned values, not exceptions. modkit follows this pattern. - -## Handler-Level Errors - -```go -func (c *Controller) GetUser(w http.ResponseWriter, r *http.Request) { - user, err := c.service.FindByID(r.Context(), id) - if err != nil { - if errors.Is(err, ErrNotFound) { - httpapi.WriteProblem(w, http.StatusNotFound, "User not found") - return - } - httpapi.WriteProblem(w, http.StatusInternalServerError, "Internal error") - return - } - json.NewEncoder(w).Encode(user) -} -``` - -## Structured Error Responses - -Use RFC 7807 Problem Details for consistent error responses: - -```go -type Problem struct { - Type string `json:"type,omitempty"` - Title string `json:"title"` - Status int `json:"status"` - Detail string `json:"detail,omitempty"` -} -``` - -## Error Middleware (Optional) - -For centralized error handling, wrap handlers: - -```go -func ErrorHandler(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer func() { - if err := recover(); err != nil { - httpapi.WriteProblem(w, 500, "Internal server error") - } - }() - next.ServeHTTP(w, r) - }) -} -``` -``` - -### Step 2: Commit - -```bash -git add docs/guides/error-handling.md -git commit -m "docs: add error handling guide" -``` - ---- - -## Task 2: Link guide from README - -**Files:** -- Modify: `README.md` - -### Step 1: Add error handling guide to the Guides list - -### Step 2: Commit - -```bash -git add README.md -git commit -m "docs: link error handling guide" -``` diff --git a/.github/internal/plans/docs/07-validation.md b/.github/internal/plans/docs/07-validation.md deleted file mode 100644 index 948df50..0000000 --- a/.github/internal/plans/docs/07-validation.md +++ /dev/null @@ -1,141 +0,0 @@ -# D7: Validation Guide - -**Status:** πŸ”΄ Not started -**Type:** New guide -**NestJS Equivalent:** Pipes - ---- - -## Goal - -Document Go‑idiomatic validation and transformation patterns as the modkit equivalent of Nest pipes. - -## Why Different from NestJS - -NestJS pipes are framework hooks that transform/validate before handlers. In Go, validation is explicit in handlers using standard library or validation libraries. This is more verbose but debuggable. - -## Files to Create/Modify - -- Create: `docs/guides/validation.md` -- Modify: `README.md` (add link) - ---- - -## Task 1: Create validation guide - -**Files:** -- Create: `docs/guides/validation.md` - -### Step 1: Draft the guide - -Include: - -1. **JSON decode + validation flow** -2. **Example with `json.Decoder` + `DisallowUnknownFields`** -3. **Optional mention of validator libraries** (no core dependency) -4. **Path parameter validation** - -Suggested structure: - -```markdown -# Validation - -modkit uses explicit validation in handlers rather than framework-level pipes. - -## JSON Request Validation - -```go -func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) { - var req CreateUserRequest - - dec := json.NewDecoder(r.Body) - dec.DisallowUnknownFields() - if err := dec.Decode(&req); err != nil { - httpapi.WriteProblem(w, http.StatusBadRequest, "Invalid JSON") - return - } - - if err := validate(req); err != nil { - httpapi.WriteProblem(w, http.StatusBadRequest, err.Error()) - return - } - - // proceed with valid request -} -``` - -## Manual Validation - -```go -func validate(req CreateUserRequest) error { - if req.Name == "" { - return errors.New("name is required") - } - if !strings.Contains(req.Email, "@") { - return errors.New("invalid email") - } - return nil -} -``` - -## Using a Validation Library - -For complex validation, consider `go-playground/validator`: - -```go -import "github.com/go-playground/validator/v10" - -var validate = validator.New() - -type CreateUserRequest struct { - Name string `json:"name" validate:"required"` - Email string `json:"email" validate:"required,email"` -} - -func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) { - var req CreateUserRequest - // ... decode ... - if err := validate.Struct(req); err != nil { - httpapi.WriteProblem(w, http.StatusBadRequest, err.Error()) - return - } -} -``` - -## Path Parameter Validation - -```go -func (c *Controller) GetUser(w http.ResponseWriter, r *http.Request) { - idStr := r.PathValue("id") - id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - httpapi.WriteProblem(w, http.StatusBadRequest, "Invalid user ID") - return - } - // proceed with valid id -} -``` -``` - -### Step 2: Commit - -```bash -git add docs/guides/validation.md -git commit -m "docs: add validation guide" -``` - ---- - -## Task 2: Link guide from README - -**Files:** -- Modify: `README.md` - -### Step 1: Add validation guide to the Guides list - -### Step 2: Commit - -```bash -git add README.md -git commit -m "docs: link validation guide" -``` diff --git a/.github/internal/plans/docs/08-auth-guards.md b/.github/internal/plans/docs/08-auth-guards.md deleted file mode 100644 index dad9931..0000000 --- a/.github/internal/plans/docs/08-auth-guards.md +++ /dev/null @@ -1,134 +0,0 @@ -# D8: Auth & Guards Guide - -**Status:** πŸ”΄ Not started -**Type:** New guide -**NestJS Equivalent:** Guards - ---- - -## Goal - -Document Go‑idiomatic auth/authorization patterns (middleware + context) as the modkit equivalent of Nest guards. - -## Why Different from NestJS - -NestJS guards are framework hooks that run before handlers and return boolean/throw. In Go, auth is implemented as middleware that: -- Validates credentials -- Sets user info in context -- Returns 401/403 or calls next handler - -## Files to Create/Modify - -- Create: `docs/guides/auth-guards.md` -- Modify: `README.md` (add link) - ---- - -## Task 1: Create auth/guards guide - -**Files:** -- Create: `docs/guides/auth-guards.md` - -### Step 1: Draft the guide - -Include: - -1. **Auth middleware example** β€” validates token, sets user in context -2. **Handler example** β€” reads user from context -3. **Role-based authorization** β€” middleware that checks roles -4. **Recommendation** β€” keep auth in middleware, not core - -Suggested structure: - -```markdown -# Authentication & Authorization - -modkit uses middleware for auth instead of framework-level guards. - -## Authentication Middleware - -```go -func AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - token := r.Header.Get("Authorization") - if token == "" { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - user, err := validateToken(token) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - ctx := context.WithValue(r.Context(), userKey, user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} -``` - -## Reading User in Handlers - -```go -func (c *Controller) GetProfile(w http.ResponseWriter, r *http.Request) { - user := UserFromContext(r.Context()) - if user == nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - // use user -} -``` - -## Role-Based Authorization - -```go -func RequireRole(role string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user := UserFromContext(r.Context()) - if user == nil || !user.HasRole(role) { - http.Error(w, "Forbidden", http.StatusForbidden) - return - } - next.ServeHTTP(w, r) - }) - } -} - -// Usage -router.Route("/admin", func(r chi.Router) { - r.Use(AuthMiddleware) - r.Use(RequireRole("admin")) - r.Get("/dashboard", adminDashboard) -}) -``` - -## Context Helpers - -See the [Context Helpers](context-helpers.md) guide for typed context key patterns. -``` - -### Step 2: Commit - -```bash -git add docs/guides/auth-guards.md -git commit -m "docs: add auth/guards guide" -``` - ---- - -## Task 2: Link guide from README - -**Files:** -- Modify: `README.md` - -### Step 1: Add auth/guards guide to the Guides list - -### Step 2: Commit - -```bash -git add README.md -git commit -m "docs: link auth/guards guide" -``` diff --git a/.github/internal/plans/docs/09-interceptors.md b/.github/internal/plans/docs/09-interceptors.md deleted file mode 100644 index e46eb90..0000000 --- a/.github/internal/plans/docs/09-interceptors.md +++ /dev/null @@ -1,163 +0,0 @@ -# D9: Interceptors Guide - -**Status:** πŸ”΄ Not started -**Type:** New guide -**NestJS Equivalent:** Interceptors - ---- - -## Goal - -Document Go‑idiomatic request/response interception using middleware and handler wrappers. - -## Why Different from NestJS - -NestJS interceptors use RxJS observables to wrap handler execution for logging, caching, and response transformation. In Go, this is achieved with: -- Middleware for request/response wrapping -- Response writers for capturing output -- Handler wrappers for timing/logging - -## Files to Create/Modify - -- Create: `docs/guides/interceptors.md` -- Modify: `README.md` (add link) - ---- - -## Task 1: Create interceptors guide - -**Files:** -- Create: `docs/guides/interceptors.md` - -### Step 1: Draft the guide - -Include: - -1. **Explanation** β€” middleware/wrappers are the Go equivalent -2. **Timing middleware** β€” measure request duration -3. **Response capture** β€” wrap ResponseWriter to capture status -4. **Caching middleware** β€” simple caching pattern - -Suggested structure: - -```markdown -# Interceptors - -In Go, interceptor patterns are implemented using middleware and response wrappers. - -## Timing Middleware - -```go -func TimingMiddleware(logger *slog.Logger) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - next.ServeHTTP(w, r) - logger.Info("request completed", - "method", r.Method, - "path", r.URL.Path, - "duration", time.Since(start), - ) - }) - } -} -``` - -## Response Status Capture - -To capture the response status code: - -```go -type statusWriter struct { - http.ResponseWriter - status int -} - -func (w *statusWriter) WriteHeader(code int) { - w.status = code - w.ResponseWriter.WriteHeader(code) -} - -func LoggingMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - sw := &statusWriter{ResponseWriter: w, status: http.StatusOK} - next.ServeHTTP(sw, r) - log.Printf("%s %s -> %d", r.Method, r.URL.Path, sw.status) - }) -} -``` - -## Caching Middleware - -```go -func CacheMiddleware(cache Cache, ttl time.Duration) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - next.ServeHTTP(w, r) - return - } - - key := r.URL.String() - if cached, ok := cache.Get(key); ok { - w.Write(cached) - return - } - - rec := httptest.NewRecorder() - next.ServeHTTP(rec, r) - - cache.Set(key, rec.Body.Bytes(), ttl) - for k, v := range rec.Header() { - w.Header()[k] = v - } - w.WriteHeader(rec.Code) - w.Write(rec.Body.Bytes()) - }) - } -} -``` - -## Response Transformation - -For transforming responses, capture and modify: - -```go -func WrapResponse(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rec := httptest.NewRecorder() - next.ServeHTTP(rec, r) - - // Transform the response - body := rec.Body.Bytes() - wrapped := map[string]any{"data": json.RawMessage(body)} - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(wrapped) - }) -} -``` -``` - -### Step 2: Commit - -```bash -git add docs/guides/interceptors.md -git commit -m "docs: add interceptors guide" -``` - ---- - -## Task 2: Link guide from README - -**Files:** -- Modify: `README.md` - -### Step 1: Add interceptors guide to the Guides list - -### Step 2: Commit - -```bash -git add README.md -git commit -m "docs: link interceptors guide" -``` diff --git a/.github/internal/plans/docs/10-context-helpers.md b/.github/internal/plans/docs/10-context-helpers.md deleted file mode 100644 index 53ee8f0..0000000 --- a/.github/internal/plans/docs/10-context-helpers.md +++ /dev/null @@ -1,139 +0,0 @@ -# D10: Context Helpers Guide - -**Status:** πŸ”΄ Not started -**Type:** New guide -**NestJS Equivalent:** Custom Decorators - ---- - -## Goal - -Document Go‑idiomatic context helper patterns as the modkit equivalent of Nest custom decorators. - -## Why Different from NestJS - -NestJS custom decorators like `@User()` or `@Roles()` use metadata reflection to extract request data. In Go, this is achieved with typed context keys and helper functions. - -## Files to Create/Modify - -- Create: `docs/guides/context-helpers.md` -- Modify: `README.md` (add link) - ---- - -## Task 1: Create context helpers guide - -**Files:** -- Create: `docs/guides/context-helpers.md` - -### Step 1: Draft the guide - -Include: - -1. **Typed context keys** β€” unexported key types -2. **Helper functions** β€” `WithUser(ctx, user)` and `UserFromContext(ctx)` -3. **Best practices** β€” keep keys unexported, return zero values safely -4. **Multiple values** β€” request ID, tenant, etc. - -Suggested structure: - -```markdown -# Context Helpers - -Go uses `context.Context` to pass request-scoped values. This guide shows how to create type-safe context helpers. - -## Typed Context Keys - -Always use unexported types for context keys to avoid collisions: - -```go -// internal/auth/context.go -package auth - -type contextKey string - -const userKey contextKey = "user" -``` - -## Helper Functions - -Provide exported functions to set and get values: - -```go -func WithUser(ctx context.Context, user *User) context.Context { - return context.WithValue(ctx, userKey, user) -} - -func UserFromContext(ctx context.Context) *User { - user, _ := ctx.Value(userKey).(*User) - return user -} -``` - -## Using in Middleware - -```go -func AuthMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - user, err := authenticate(r) - if err != nil { - http.Error(w, "Unauthorized", 401) - return - } - ctx := auth.WithUser(r.Context(), user) - next.ServeHTTP(w, r.WithContext(ctx)) - }) -} -``` - -## Using in Handlers - -```go -func (c *Controller) GetProfile(w http.ResponseWriter, r *http.Request) { - user := auth.UserFromContext(r.Context()) - if user == nil { - http.Error(w, "Unauthorized", 401) - return - } - json.NewEncoder(w).Encode(user) -} -``` - -## Common Context Values - -| Value | Package | Setter | Getter | -|-------|---------|--------|--------| -| User | `auth` | `WithUser` | `UserFromContext` | -| Request ID | `requestid` | `WithRequestID` | `RequestIDFromContext` | -| Tenant | `tenant` | `WithTenant` | `TenantFromContext` | - -## Best Practices - -1. **Keep keys unexported** β€” prevents external packages from accessing directly -2. **Return nil/zero safely** β€” check for nil in getters -3. **Don't store large objects** β€” context is copied frequently -4. **Use for request-scoped data only** β€” not for dependency injection -``` - -### Step 2: Commit - -```bash -git add docs/guides/context-helpers.md -git commit -m "docs: add context helpers guide" -``` - ---- - -## Task 2: Link guide from README - -**Files:** -- Modify: `README.md` - -### Step 1: Add context helpers guide to the Guides list - -### Step 2: Commit - -```bash -git add README.md -git commit -m "docs: link context helpers guide" -``` diff --git a/.github/internal/plans/docs/99-post-mvp-roadmap.md b/.github/internal/plans/docs/99-post-mvp-roadmap.md deleted file mode 100644 index fa6879d..0000000 --- a/.github/internal/plans/docs/99-post-mvp-roadmap.md +++ /dev/null @@ -1,111 +0,0 @@ -# Post-MVP Roadmap - -**Status:** ⏭️ Deferred -**Type:** Roadmap / Future planning - ---- - -## Purpose - -This document defines the post-MVP direction for modkit with a clear architectural north star. The core will remain Go-idiomatic and minimal (Option 1). Ergonomics and scaffolding will live in separate, optional packages (`modkitx` and `modkit-cli`). Implementation scheduling is intentionally deferred until the MVP documentation is complete. - ---- - -## Architectural North Star: Go-Idiomatic Minimal Core - -The core package (`modkit`) is responsible for only the essential, stable primitives: -- Module metadata (`imports`, `providers`, `controllers`, `exports`) -- Deterministic graph construction with visibility enforcement -- Singleton provider container with explicit resolution -- Explicit controller registration without reflection - -The core must avoid runtime magic, decorators, and hidden conventions. It should be small, predictable, and debuggable. API changes should be conservative and motivated by clarity or correctness, not convenience. - -### In Scope for Core - -- Module model and validation -- Kernel graph, visibility, container, and bootstrap -- Minimal HTTP adapter (routing + server glue) - -### Out of Scope for Core - -- Scaffolding or generation tooling -- Opinionated defaults (config, logging, metrics) -- Additional adapters (gRPC, jobs) -- Helper DSLs or builders - ---- - -## Companion Package: modkitx (Ergonomics Layer) - -`modkitx` is an optional helper package that reduces boilerplate without changing semantics. It should only wrap or generate the same metadata that core consumes. It must not introduce reflection, auto-wiring, or hidden registration. - -### Goals - -- Provide a small builder API that compiles to `module.Module` and produces `ModuleDef` -- Add common provider helpers (e.g., value provider, func provider) -- Provide optional HTTP helper middleware (logging, request IDs, standardized error helpers) - -### Constraints - -- All helpers must be explicit and deterministic -- The kernel behavior must remain unchanged; only ergonomics improve -- `modkitx` should be fully optional for adoption - -### Quality Bar - -- Unit tests that assert the builder output equals a hand-written `ModuleDef` -- Middleware helpers are opt-in and do not change behavior unless attached - ---- - -## Companion Package: modkit-cli (Scaffolding Tooling) - -`modkit-cli` is a developer productivity tool for generating skeletons and boilerplate. It must never be required at runtime. Generated code should compile cleanly and use only public APIs from `modkit` (and optionally `modkitx`). - -### Goals - -- Generate module scaffolds (`module.go`, `controller.go`, `routes.go`, `service.go`) -- Generate a minimal app bootstrap with an HTTP server -- Produce code that follows modkit conventions by default but is fully editable - -### Constraints - -- No runtime dependency on the CLI after generation -- Templates should be simple and stable before adding complex project inspection - -### Quality Bar - -- Golden-file tests for templates -- Generated code compiles and passes a minimal test suite - ---- - -## Future Adapters - -### gRPC Adapter - -Add `modkit/grpc` as a thin adapter similar to `modkit/http`: -- Controllers implement a gRPC registration interface -- No reflection-based service discovery -- Works with protoc-generated code - -### Job Runner - -Add `modkit/jobs` for background job processing: -- Jobs as providers with explicit registration -- Simple in-process runner for MVP -- Optional external queue integration later - ---- - -## Sequencing Notes - -Implementation scheduling will be defined after the MVP documentation is complete. This document only fixes direction and package boundaries, not timeline or task breakdown. - -Priority order (when ready): -1. Complete MVP documentation (D1-D10) -2. modkitx builder API -3. modkit-cli scaffolding -4. gRPC adapter -5. Job runner diff --git a/.github/internal/plans/sdlc/01-commit-validation.md b/.github/internal/plans/sdlc/01-commit-validation.md deleted file mode 100644 index 667f2ea..0000000 --- a/.github/internal/plans/sdlc/01-commit-validation.md +++ /dev/null @@ -1,241 +0,0 @@ -# S1: Commit Validation with Lefthook + Go Commitlint - -## Status - -🟒 Complete - -## Overview - -Add automated commit message validation using Go-native tooling to enforce conventional commits format. This provides immediate feedback to developers and enables future automation of changelogs and releases. - -## Goals - -1. Enforce conventional commit format at commit time -2. Use Go-native tools (no Node.js dependency) -3. Minimal setup friction for contributors -4. Enable future changelog/release automation - -## Non-Goals - -- Pre-commit hooks for formatting/linting (deferred to S4) -- CI-based commit validation (local validation is sufficient) -- Custom commit message templates - ---- - -## Tools - -### Lefthook - -- **Purpose**: Git hooks manager written in Go -- **Why**: Fast, single binary, no runtime dependencies -- **Install**: `go install github.com/evilmartians/lefthook@latest` -- **Docs**: https://github.com/evilmartians/lefthook - -### conventionalcommit/commitlint - -- **Purpose**: Validates commit messages against conventional commits spec -- **Why**: Go-native, simple CLI -- **Install**: `go install github.com/conventionalcommit/commitlint@latest` -- **Docs**: https://github.com/conventionalcommit/commitlint - ---- - -## Implementation - -### 1. Create lefthook.yml - -Configuration file at repository root: - -```yaml -# Lefthook configuration for git hooks -# Setup: make setup-hooks - -commit-msg: - commands: - commitlint: - run: '"${GOBIN:-$(go env GOPATH)/bin}/commitlint" lint --message "{1}"' -``` - -**Notes:** -- `{1}` is Lefthook's placeholder for the commit message file path -- The hook runs on both `git commit` and `git commit --amend` by default - -### 2. Create tools/tools.go - -Track development tool dependencies using the Go tools pattern: - -```go -//go:build tools -// +build tools - -// Package tools tracks development tool dependencies. -package tools - -import ( - _ "github.com/conventionalcommit/commitlint" - _ "github.com/evilmartians/lefthook" - _ "github.com/golangci/golangci-lint/cmd/golangci-lint" - _ "golang.org/x/tools/cmd/goimports" - _ "golang.org/x/vuln/cmd/govulncheck" -) -``` - -Then run `go mod tidy` to add these to `go.mod` with pinned versions. - -### 3. Update Makefile - -Add targets for setup and validation: - -```makefile -# Install all development tools (tracked in tools/tools.go) -.PHONY: tools -tools: - @echo "Installing development tools..." - @cat tools/tools.go | grep _ | awk '{print $$2}' | xargs -I {} sh -c 'go install {}' - @echo "βœ“ All tools installed" - -# Install development tools and setup git hooks -setup-hooks: tools - @echo "Setting up git hooks..." - lefthook install - @echo "βœ“ Git hooks installed successfully" - -# Validate a commit message (for CI or manual testing) -lint-commit: - @echo "$(MSG)" | $(COMMITLINT) lint -``` - -**Usage:** -```bash -# Install all tools -make tools - -# Setup hooks (one-time after clone, runs 'make tools' first) -make setup-hooks - -# Manual validation (testing) -make lint-commit MSG="feat: add new feature" -``` - -### 4. Update CONTRIBUTING.md - -Add to "Getting Started" section after "Prerequisites": - -```markdown -### Setup Git Hooks - -After cloning the repository, run once to enable commit message validation: - -\`\`\`bash -make setup-hooks -\`\`\` - -This installs git hooks that validate commit messages follow the [Conventional Commits](https://www.conventionalcommits.org/) format. - -If you see a commit validation error, ensure your message follows this format: - -\`\`\` -(): - - - - -\`\`\` - -Examples: -- `feat: add user authentication` -- `fix(http): handle connection timeout` -- `docs: update installation guide` -``` - ---- - -## Validation Rules - -### Enforced by commitlint - -1. **Header format**: `(): ` - - Scope is optional - - Max 50 characters for full header (per CONTRIBUTING.md) - -2. **Valid types**: - - `feat` - New feature - - `fix` - Bug fix - - `docs` - Documentation changes - - `test` - Test changes - - `chore` - Build/tooling changes - - `refactor` - Code refactoring - - `perf` - Performance improvements - - `ci` - CI/CD changes - -3. **Description rules**: - - Lowercase - - No period at end - - Imperative mood ("add" not "added") - -4. **Breaking changes**: - - Add `!` after type/scope: `feat!: breaking change` - - Or use footer: `BREAKING CHANGE: description` - ---- - -## Testing - -### Manual Testing - -Test the hook installation: - -```bash -# 1. Setup hooks -make setup-hooks - -# 2. Try a bad commit message -git commit -m "bad message" -# Should fail with validation error - -# 3. Try a good commit message -git commit -m "feat: test commit validation" -# Should succeed -``` - -### Bypass (for emergencies only) - -```bash -# Skip hooks if absolutely necessary -git commit --no-verify -m "emergency fix" -``` - ---- - -## Migration Notes - -### Existing Contributors - -Contributors who already have the repository cloned should: - -1. Pull the latest changes -2. Run `make setup-hooks` once -3. Continue normal workflow - -### CI/CD - -No changes needed - validation happens locally at commit time. - ---- - -## Future Extensions - -Once S1 is complete, this foundation enables: - -- **S2**: Changelog generation from commit messages -- **S3**: Automated releases with semantic versioning -- **S4**: Pre-commit hooks for `make fmt && make lint` - ---- - -## References - -- [Conventional Commits Specification](https://www.conventionalcommits.org/) -- [Lefthook Documentation](https://github.com/evilmartians/lefthook) -- [Semantic Versioning](https://semver.org/) diff --git a/AGENTS.md b/AGENTS.md index 998e002..d53a5c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,10 @@ modkit/ β”œβ”€β”€ examples/ # Example applications β”‚ β”œβ”€β”€ hello-simple/ # Minimal example (no dependencies) β”‚ └── hello-mysql/ # Full CRUD example with DB -β”œβ”€β”€ docs/ -β”‚ β”œβ”€β”€ guides/ # User guides -β”‚ β”œβ”€β”€ reference/ # API reference -β”‚ └── architecture.md # How modkit works -└── .github/internal/plans/ # Implementation tracking (internal) +└── docs/ + β”œβ”€β”€ guides/ # User guides + β”œβ”€β”€ reference/ # API reference + └── architecture.md # How modkit works ``` ## Development Workflow @@ -70,31 +69,13 @@ make test # Run all tests (must pass) - Bootstrap real modules in integration tests - Use testcontainers for smoke tests with external deps -## Commit Format +## Commits and Pull Requests -Follow [Conventional Commits](https://www.conventionalcommits.org/): -```text -(): - -[optional body] -``` - -Valid types: `feat`, `fix`, `docs`, `test`, `chore`, `refactor`, `perf`, `ci` - -**Examples:** -- `feat(http): add graceful shutdown to Serve` -- `fix(kernel): detect provider cycles correctly` -- `docs: add lifecycle guide` - -## Pull Requests - -See [.github/pull_request_template.md](.github/pull_request_template.md) for the complete template. +See [CONTRIBUTING.md](CONTRIBUTING.md) for commit format (Conventional Commits) and PR requirements. -**Key requirements:** -1. Run `make fmt && make lint && make test` before submitting -2. Check all applicable type boxes -3. Complete the validation section with command outputs -4. Document any breaking changes +**Quick reference:** +- Valid types: `feat`, `fix`, `docs`, `test`, `chore`, `refactor`, `perf`, `ci` +- Run `make fmt && make lint && make test` before submitting ## Documentation diff --git a/README.md b/README.md index 28acd7b..9a92a34 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,10 @@ modkit is in **early development**. APIs may change before v0.1.0. After v0.1.0, changes will follow semantic versioning. +## Community + +Questions? Start a [Discussion](https://github.com/aryeko/modkit/discussions). + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). We welcome issues, discussions, and PRs.