// Package module is MyDay's module registry (SPEC-008): the module CONTRACT, // the runtime registry, the build-time DISCOVERY seam, and the core Modules // page. // // Discovery is at BUILD time, not runtime (D-028, the Caddy pattern): a module // is a package under internal/modules// that self-registers from its // init() by calling Add; a generated aggregator (internal/modules/imports_gen.go, // produced by `go generate`) blank-imports every module directory, so THE // DIRECTORY IS THE SOURCE OF TRUTH. Adding a module is: add the directory → // `go generate` → rebuild. No core edits. // // The registry makes the loaded set explicit and deterministic: modules keep // their registration order (never map iteration), the loaded set is logged at // startup, and enable/disable is config-backed (D-038) — a disabled module is // not mounted, so its routes 404; its data is never deleted. // // Spec: docs/specs/spec-008-module-registry.md. package module import ( "context" "strings" "sync" "git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/server" "git.lohmar.co.uk/cclohmar/NextMyDay/internal/core/store" ) // Migration is one ordered, named schema migration owned by a module. It reuses // the core runner's type so the CORE owns the runner (SPEC-008 §4/AC-5): the // registry prefixes each version with the module name, avoiding collisions with // the core schema and between modules. type Migration = store.Migration // Handler is a dispatched module function. It is the door's handler signature: // the module receives the resolved RequestContext — identification and routing, // never a raw path (the rule of the door, AGENT.md §3.2). type Handler = server.Handler // Route is one path-mode function a module declares (SPEC-008 §4, §4.2): // `/{GUID}/{module}/{name}/…` resolves to Function. type Route struct { // Name is the PARAM3 value the route answers (e.g. "pto"). Ignored when // Default is set. Name string // Function is the resolved function name the door dispatches on. Function string // Handler runs the function. Handler Handler // Default marks the route served when PARAM3 is absent or trailing // (SPEC-002 §3.4): a module declares at most one. Default bool } // Action is one action-mode function a module declares (SPEC-008 §4, §4.3): // `POST /{GUID}?do={module}&action={name}` resolves to Function. type Action struct { // Name is the `action` value the module answers. Name string // Function is the resolved function name the door dispatches on. Function string // Handler runs the function. Handler Handler } // Module is the module contract every module implements (SPEC-008 §4). The // default skeleton lives in internal/modules// as app.go (this manifest) · // routing.go (Routes/Actions) · handlers.go (the handlers), per D-037. type Module interface { // Name is the PARAM2 segment — the registry key ("home", "time", …). Name() string // Required reports whether the module is always on. A required module // cannot be disabled (D-038, AC-4). Required() bool // Migrations is the module's own schema, versioned and applied in // registration order by the core runner (AC-5). Migrations() []Migration // Routes are the module's path-mode functions (§4.2). Routes() []Route // Actions are the module's action-mode functions (§4.3). Actions() []Action // Register adds the module to the runtime registry. The loader calls it // once per discovered module; the usual implementation is `r.Add(m)`. Register(r *Registry) error // Start is OPTIONAL background work (integrations, sync). A module without // background work returns nil. Start(ctx context.Context) error } // Kind is an OPTIONAL capability a module may implement to declare its // catalogue kind (SPEC-008 §4.8: records · domain · workflow · analytical · // integration). The Modules page lists it (§7); absent, KindOf reports // "module". type Kind interface { Kind() string } // KindOf returns a module's kind for the Modules page, defaulting to "module" // when the module does not implement Kind. func KindOf(m Module) string { if k, ok := m.(Kind); ok { if s := strings.TrimSpace(k.Kind()); s != "" { return s } } return "module" } // ── build-time discovery (D-028) ───────────────────────────────────────────── // discovered is the process-wide self-registration list. Modules append to it // from init(); because the generated aggregator blank-imports every module // directory IN SORTED ORDER, Go runs those init() functions in that order and // the list is deterministic (D-028 guardrail: never map iteration). var ( discoveryMu sync.Mutex discovered []Module ) // Add self-registers a module from its package init() (D-028). It is called by // each module directory; the generated aggregator guarantees the package is // imported, so the directory is the source of truth. // // A duplicate name is not detected here (init cannot fail usefully): the // runtime Add catches it when Load builds the registry, which is loud and // testable. func Add(m Module) { if m == nil { return } discoveryMu.Lock() discovered = append(discovered, m) discoveryMu.Unlock() } // Discovered returns the process-wide, self-registered modules in registration // order. It returns a copy: the caller may not mutate the registry through it. func Discovered() []Module { discoveryMu.Lock() defer discoveryMu.Unlock() out := make([]Module, len(discovered)) copy(out, discovered) return out }