docs(04): research phase implementation
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Phase 4 Research — CLI scaffolding, i18n and mail
|
||||
|
||||
**Researched:** 2026-09-18
|
||||
**Scope:** `summercms.go` only
|
||||
**Requirements:** CLI-02, I18N-01, I18N-03
|
||||
|
||||
## Summary
|
||||
|
||||
The phase has three user-visible slices: a developer generates and builds a plugin artifact, a plugin resolves Polish and English strings, and a plugin renders and sends a registered mail template. The binding decisions are in `04-CONTEXT.md` D-01–D-21. The existing `internal/build` and `cmd/summer` code already provides the first `make:plugin` path; `pact` capability interfaces, `backpack` services, and `party.Activate` provide the integration points for the other two slices. No change to `../fonoteka.go` is required.
|
||||
|
||||
The major implementation risk is generated Go code crossing plugin subpackage boundaries. Generate `registry.gen.go` from a deterministic artifact manifest or parsed declarations, format it with `go/format`, and compile a temporary `examples/hello` plugin after every artifact type. A second risk is using go-i18n's message templating when PHP `:name` placeholders must remain literal. Let go-i18n select only a CLDR category, then substitute PHP placeholders in phrasebook. Mail's main risk is treating rendered Markdown or layout HTML as trusted without checking the escaping boundary.
|
||||
|
||||
## Existing code and seams
|
||||
|
||||
| Concern | Existing implementation | Phase 4 extension |
|
||||
|---|---|---|
|
||||
| CLI | `cmd/summer/main.go` registers `bonfire.Command` values; `make:plugin` invokes `build.MakePlugin` | Add five `make:*` commands with the existing output/error convention; permit plugin ID omission only when `pluginIDFromGo` identifies the current plugin. |
|
||||
| Scaffold | `internal/build/scaffold.go` writes `plugin.go` and `go.mod` with Go string builders; `build.App` generates app entry points and runs `go build` | Move source builders to embedded `text/template` files, create Winter directory shape, generate root registry accessors, and run a pre-build leaf-import check. Preserve `AddPlugin` and app manifest behavior. |
|
||||
| Capabilities | `pact/capabilities.go` has `HasConfig`, `HasMigrations`, `HasCommands`, `HasRoutes` | Add `HasLang` and `HasMailTemplates` as `fs.FS` capabilities, plus minimal job and admin-controller interfaces from D-14/D-15. Compose generated migrations/commands through capability methods in scaffolded `plugin.go`. |
|
||||
| Boot and services | `backpack.New(cfg)` holds services; `party.Activate` orders plugins and calls Register then Boot | Publish phrasebook translator and postcard mailer through backpack; load plugin assets in activation order, and fail boot with plugin ID plus missing template/layout name. Avoid requiring existing handwritten plugins to opt in. |
|
||||
| Tests | `internal/build/build_test.go`, `examples/hello`, `go test ./...`; Phase 3 uses `testcontainers-go` for Postgres | Generate all six artifact types in a temp copy of `examples/hello`, then run `go build` and `go vet` there. Add focused phrasebook/postcard tests, plus a Mailpit SMTP integration test under the existing `-short`/Docker convention. |
|
||||
|
||||
The directory layout and `models/` leaf rule in `.planning/notes/plugin-layout-winter-directories.md` are binding. The Phase 3 Fonoteka plugin layout is intentionally deferred; the leaf check must apply to new plugin packages without rewriting that repo.
|
||||
|
||||
## Library findings
|
||||
|
||||
### Translations
|
||||
|
||||
[go-i18n v2.6.1's documented API](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2@v2.6.1/i18n) exposes `i18n.Message{Zero, One, Two, Few, Many, Other}` and `LocalizeConfig.PluralCount`. Its bundle/localizer selects the CLDR plural variant. The repository's `.planning/research/STACK.md` already selects v2.6.1; keep that version for this phase unless a concrete incompatibility appears. To honor D-03, feed category labels such as `"one"`, `"few"` and `"many"` as the message variants, read the chosen label, then select the original YAML string and perform `:name`, `:Name`, `:NAME` substitution locally. This keeps go-i18n's Go-template evaluation away from PHP translation text. Validate categories against each locale's actual categories and require `other` for maps. Test Polish counts 1/2/5/22 and English 1/2, including 0 and fractional counts.
|
||||
|
||||
[goccy/go-yaml](https://pkg.go.dev/github.com/goccy/go-yaml) already appears in the project's chosen stack and can decode nested maps. Decode into string-keyed data, reject non-string leaves and duplicate full paths, and flatten `group.dot.path`. Treat a map as a plural map only when its keys are CLDR category names; otherwise recurse. This disambiguates nested namespaces from plural values. `fs.WalkDir` over `HasLang.LangFS()` can identify `lang/<locale>/<group>.yaml` files. Build each plugin's namespace before merging so duplicate ownership fails boot instead of silently replacing another plugin's translation.
|
||||
|
||||
Locale fallback is an explicit phrasebook sequence: requested tag, parent tag, `app.fallback_locale`, then raw key. Go's language matching can help parse tags, but the fallback order must remain D-04's order. The configured default/fallback are `en`; no framework Polish default. Request-context locale resolution is Phase 7, so expose a context accessor now without adding user lookup.
|
||||
|
||||
Laravel pipe forms need a separate parser: explicit `{n}` and `[a,b]` conditions win before category-order forms; only then use the CLDR category result. Test malformed brackets and missing forms as boot-time errors with plugin/file/key.
|
||||
|
||||
### Mail
|
||||
|
||||
[goldmark's documented default](https://github.com/yuin/goldmark) rejects raw HTML and potentially dangerous links unless `html.WithUnsafe` is enabled. Keep that default. D-07's render sequence remains `html/template` on Markdown source, then Goldmark to HTML; the rendered Markdown is the text part. Add a test with variable values containing HTML tags, Markdown link syntax, and `javascript:` URLs. `html/template` protects HTML syntax but does not necessarily neutralize Markdown syntax before Goldmark parses it, so assert the *final* HTML and mail body are safe. Any `template.HTML` used for the trusted, generated HTML part in a layout must be constructed internally, never from caller variables. Use `html/template` for subject and layout rendering too; ensure subjects cannot contain CR/LF header injection.
|
||||
|
||||
[go-mail's current API](https://pkg.go.dev/github.com/wneessen/go-mail) provides `mail.NewClient(host, opts...)`, `WithPort`, TLS policy options, and `DialAndSendWithContext`; `Msg.SetBodyString` supports both plain text and HTML parts. The SMTP driver should use context-aware sending, explicit TLS behavior from `mail.*`, and structured recipient setters. Mailpit's local SMTP endpoint needs an explicit `NoTLS` policy, while production must not silently downgrade. Do not log credentials or full message bodies from SMTP errors. The log driver logs rendered headers and text in dev as D-18 requests, with its use controlled by config.
|
||||
|
||||
[Mailpit's API documentation](https://mailpit.axllent.org/docs/api-v1/) confirms a REST API for stored messages and an interactive `/api/v1/` endpoint. Use a testcontainer with mapped SMTP and HTTP ports. Send through the real go-mail SMTP driver, then poll the HTTP API with a bounded deadline for the recipient/subject and parts. `testcontainers-go` exposes `WithExposedPorts` and mapped ports in its [Go API](https://pkg.go.dev/github.com/testcontainers/testcontainers-go). Pin the Mailpit image by tag in the implementation and keep the integration test out of `go test -short`.
|
||||
|
||||
The parser must distinguish a template's header and Markdown body (`==`) from a layout's header, text wrapper and HTML wrapper (three sections as shown by the PHP reference). Registration should validate path/name mapping and layout references at Boot. D-08 says locale suffixes are literal names selected by the caller; postcard must not add fallback lookup.
|
||||
|
||||
### Scaffolding
|
||||
|
||||
The existing `MakePlugin` already validates `vendor.plugin`, finds the app root, writes `go.mod`, runs `go mod tidy`, and pins toolchain. Keep this behavior. Add `internal/build/stubs/*.tmpl` as `go:embed` assets. Render into memory, pass every Go file through `go/format`, and only then write files. Use stable sorted registry entries and atomic write/rename for `registry.gen.go` so repeated commands are idempotent and a failed template does not leave a partial registry. Refuse duplicate artifact names before writes.
|
||||
|
||||
The generated root `plugin.go` should call generated accessors for migrations and commands, and expose job/admin-controller/model slices as minimum interfaces now. Existing handwritten plugins get the one-time printed accessor line D-12 specifies. Avoid package import cycles: root imports `models/`, `console/`, `jobs/`, `controllers/`, `updates/`; `models/` imports no sibling. A pre-build check can parse each plugin's `models/*.go` imports using `go/parser` and match against its own module path plus sibling directory segment; report plugin ID, source file and offending import. Check embedded assets in `lang/` and `views/mail/` compile when present; Go `embed` patterns fail for empty directories, so scaffold placeholder files that match the actual embed patterns.
|
||||
|
||||
The model stub needs a migration with an explicit table and up/down callbacks. Use `gormigrate.Migration` as the existing Phase 3 capability does, not `AutoMigrate`. The admin-controller stub carries only ID/model/config-directory and YAML placeholders; the job stub carries `Kind()` and `Work(ctx,args)` without River imports. Test both standalone generation and the sequence that appends several entries to one registry.
|
||||
|
||||
## Recommended plan structure
|
||||
|
||||
Use MVP slices, because ROADMAP declares Phase 4 `Mode: mvp`: (1) scaffold and build a plugin with all generated artifact types; (2) load and resolve translations from an embedded plugin; (3) render and send registered mail with memory and SMTP drivers; (4) final unit/integration gate. A test or fixture may start a plan, but each implementation task should leave a developer-visible working path. The final plan should own the complete phase test gate, in keeping with prior phases' unit-test-last convention. Keep root/app checks separate; no `../fonoteka.go` edits.
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
| Requirement | Automated proof | Fast feedback | Full gate |
|
||||
|---|---|---|---|
|
||||
| CLI-02 | Generate plugin/model/migration/command/job/admin-controller into a temp `examples/hello` copy; `go build` and `go vet` it; inspect generated registry, YAML assets and imports | `go test ./internal/build ./cmd/summer` | `go vet ./... && go test ./...` plus dedicated scaffold compile test |
|
||||
| I18N-01 | Embedded pl/en YAML, nested and plural keys, `:name` case variants, pl-PL→pl→en fallback, raw missing key, malformed YAML/pipe failures | `go test ./phrasebook` | Root vet/test and hello plugin smoke |
|
||||
| I18N-03 | Memory driver assertions for subject, Markdown text, HTML/layout, recipients and boot errors; malicious variable cases; real go-mail SMTP delivery observed through Mailpit HTTP API | `go test ./postcard -short` | `go test ./postcard -run TestSMTPMailpit -count=1` plus root vet/test |
|
||||
|
||||
Sampling: run focused package tests after each task and root `go vet ./... && go test ./...` after each wave. The Mailpit test must fail if Docker is unavailable when run normally and skip only under `-short`, matching the project testcontainers convention. Add `go test -race ./phrasebook ./postcard ./internal/build` at the final gate if quick checks are green. Test boot failure messages and the `models/` sibling-import rejection as part of CLI-02, not only the happy path.
|
||||
|
||||
## Threats and pitfalls to carry into plans
|
||||
|
||||
- **Generated source/import tampering:** validate artifact names as Go identifiers and file paths, reject traversal and module escape, format before write, and enforce `models/` as leaf. Do not rewrite handwritten `plugin.go`.
|
||||
- **Translation namespace collision or parser ambiguity:** reject duplicate plugin IDs/keys and malformed plural maps/pipe conditions at boot with file/key context.
|
||||
- **Mail content injection:** test final Markdown-derived HTML, unsafe links, layout content trust, subject CR/LF, and recipient validation. Do not enable Goldmark `WithUnsafe`.
|
||||
- **SMTP transport downgrade/secrets:** choose TLS policy explicitly; keep passwords out of log/errors; propagate driver failure through `Send`.
|
||||
- **False green integration:** assert Mailpit API receipt after SMTP send; `go test -short` is not the full sign-off.
|
||||
|
||||
## Source links
|
||||
|
||||
- [go-i18n v2.6.1 package API](https://pkg.go.dev/github.com/nicksnyder/go-i18n/v2@v2.6.1/i18n)
|
||||
- [goccy/go-yaml package API](https://pkg.go.dev/github.com/goccy/go-yaml)
|
||||
- [goldmark README and security defaults](https://github.com/yuin/goldmark)
|
||||
- [go-mail package API](https://pkg.go.dev/github.com/wneessen/go-mail)
|
||||
- [Mailpit API documentation](https://mailpit.axllent.org/docs/api-v1/)
|
||||
- [testcontainers-go package API](https://pkg.go.dev/github.com/testcontainers/testcontainers-go)
|
||||
Reference in New Issue
Block a user