Files
summercms/.planning/phases/04-cli-scaffolding-i18n-and-mail/04-RESEARCH.md
2026-09-18 12:43:41 +02:00

85 lines
13 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.
# 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)