NextMyDay/docs/specs/spec-008-module-registry.md
Claus Lohmar 57610cdbae docs: add the project plan, M0/M0.1 specs, agent guide and README
- PROJECT_PLAN.md: the decision log of record through D-042 and the M0.1 slice
- docs/specs: SPEC-002…009 — routing · door · identity/sessions · /setup ·
  enroll/login · roles/chain/recovery · module registry · the web layer & PWA
  (plus docs/specs/README.md, the index)
- AGENT.md: the working rules — commit AND push at every submilestone (gate
  green), Conventional Commits, never commit secrets
- README.md: the quickstart, the first-run/SSH-tunnel steps and the
  localhost/loopback caveat (SPEC-009 §13 Q-3): with an https base URL the
  CSRF/session cookies are Secure, so reach the app at localhost/127.0.0.1
- config.example.yaml: the settings template (D-035)
2026-10-14 10:53:44 +00:00

107 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SPEC-008 — module registry & the Modules page
| Field | Value |
|---|---|
| **Status** | **Implemented** (2026-10-13) — see §9; `build`/`vet`/`test`/`-race` green · static binary · live-smoked |
| **Created** | 2026-10-13 |
| **Author** | OpenArchitect |
| **Target milestone** | **M0** |
| **Governing decisions** | **D-027** · **D-028** · **D-036** · **D-037** · **D-038** · `PROJECT_PLAN.md` §4.6–4.8 |
| **Related** | SPEC-002 (routing) · SPEC-003 (door) |
---
## 1. Goal
The **module registry** — build-time discovery, the module contract, and the core **Modules** page
that **enables/disables** optional modules. Modules are **compiled in**, activated at runtime.
## 2. Non-goals
Module internals (each module owns its routes, handlers, tables). Third-party/WASM plugins (future).
## 3. Discovery (D-028) — the Caddy pattern
- A module is a package under `internal/modules/<name>/` that **self-registers** via `init()`.
- A **generated aggregator** (`internal/modules/imports_gen.go`, from `go generate`) blank-imports
every module dir — **the directory is the source of truth**.
- Adding a module = add the dir → `go generate` → rebuild. **No core edits.**
- **Deterministic order** (defined sequence, never map iteration) — migrations and precedence depend
on it. **Log the loaded set at startup.**
## 4. The module contract (`app.go`)
```go
type Module interface {
Name() string // the PARAM2 segment
Required() bool // false = optional (can be disabled)
Migrations() []Migration // the module's own schema, versioned
Routes() []Route // path-mode functions (§4.2)
Actions() []Action // handler-mode functions (§4.3)
Register(r *Registry) error
Start(ctx context.Context) error // optional — background work (integrations)
}
```
## 5. The module skeleton (D-037)
```
internal/modules/expense/
app.go the manifest (above)
routing.go routes + actions
handlers.go the action handlers
queries.go its own tables (sqlc) — as needed
migrations/ its own schema (embedded) — as needed
web/ its templates/partials — as needed
```
## 6. Enable / disable (D-038)
- State lives in **`config.yaml`** → `modules:` map (D-035).
- **Disabled** → the module is **not in the registry**: its routes return **404**, its nav is hidden.
**Its data is never deleted.** Re-enabling restores it.
- **Required** modules (the core; `requests` per the "must-have" call) **cannot be disabled**.
- **Toggles are audited** (A11). Applied at **startup**; a live remount is out of scope.
## 7. The Modules page (core)
- Lists every module: name · kind · required · enabled.
- Toggling writes `config.yaml` (the D-035 caveat: needs write access, else "edit and restart").
- A required module shows as **locked**.
## 8. Acceptance criteria
| # | Criterion |
|---|---|
| AC-1 | A new module dir + `go generate` + rebuild → it appears, with **no core edits** |
| AC-2 | Registration order is deterministic; the loaded set is logged at startup |
| AC-3 | Disabling a module → its routes 404 and its nav hides; **its tables are intact** |
| AC-4 | A **required** module cannot be disabled (API + UI) |
| AC-5 | Each module's migrations run in registration order; the core owns the runner |
| AC-6 | Every enable/disable is audited (A11) |
---
## 9. Implementation notes (accepted rulings)
Built 2026-10-13. `go build` · `go vet` · `go test` (incl. `-race`) green — old and new
packages; static CGO-free binary; live-smoked (startup logs the loaded set; setup mode still serves
only `/setup`). `internal/core/routing` purity intact.
**Files:** `internal/core/module/` (the contract + registry + Modules page) ·
`internal/modules/{generate.go,imports_gen.go,gen/}` (the `go generate` discovery tool) ·
`internal/modules/home/` (the reference required module) · `cmd/myday/main.go` (wiring).
| # | Choice made by the builder | Ruling |
|---|---|---|
| **D-1** | The contract lives in `internal/core/module`; a module's manifest is `app.go`. | **Accepted** — matches D-036/D-037. |
| **D-2** | `Routes()`/`Actions()` are **declarative**; `Register(r)` is the self-registration hook. | **Accepted.** |
| **D-3** | `module` imports `server` (for `Handler`/`RequestContext`); no cycle, but the registry depends on the door. | **Accepted for M0**; a neutral `handoff` package to invert the layering is a **follow-up**. |
| **D-4** | `Kind()` is optional (`KindOf`); default `module`. | **Accepted.** |
| **D-5** | Migrations run for **enabled** modules only, versions prefixed `<module>/<version>`; disabling never drops tables. | **Accepted.** |
| **D-6** | The Modules page is **admin-only** (`GET /{GUID}/core/modules` · `POST /{GUID}?do=core&action=modules.toggle`). | **Accepted** — only the admin toggles modules. |
| **D-7** | Toggles write `config.yaml` and apply **at next start** (D-038). | **Accepted.** |
| **D-8** | `Registry.Nav()` is the nav seam; rendering is SPEC-009. | **Accepted.** |
| **D-9** | Core dispatch modules take precedence over a same-named discovered module; **no guard yet**. | **Fix** — add a **name-collision guard** (a colliding discovered module must **fail to load**, error + log). **Follow-up.** |
**Step 0 (R-7) landed with this slice** — recovery approval authority is the **chain**: `users.CanApprove(approver, requester, approverIsAncestor)`, with the walk in `recovery.isAncestor`. `users_test.go` and `spec007_recovery_test.go` cover it.