---
phase: 04-cli-scaffolding-i18n-and-mail
verified: 2026-09-18T12:22:19Z
status: passed
score: 3/3 must-haves verified
overrides_applied: 0
human_verification: []
decision_coverage:
honored: 21
total: 21
not_honored: []
---
# Phase 4: CLI scaffolding, i18n and mail Verification Report
**Phase Goal:** Scaffolding commands generate compiling stubs for every plugin artifact type, and plugins can register translated, CLDR-pluralized, namespaced strings and mail templates rendered through a pluggable driver interface.
**Verified:** 2026-09-18T12:22:19Z
**Status:** passed
**Re-verification:** No — initial verification
Must-haves are the three ROADMAP success criteria (they override plan-level truths). Plan `must_haves` (D-01…D-21) were checked as supporting evidence. The ROADMAP goal is an infrastructure/tooling outcome, not a product UI; MVP developer-flow coverage is mapped from Plan 01's objective below.
`04-REVIEW.md` status is `issues` (4 warnings, 3 info, 0 critical). Those findings do not break must-haves or the phase goal and are recorded as non-blocking notes.
## User Flow Coverage
User story (Plan 01 objective): «As a plugin developer, I want to generate compiling plugin artifacts, resolve translated strings, and send registered mail, so that I can port WinterCMS plugins into one SummerCMS binary.»
| Step | Expected | Evidence | Status |
|------|----------|----------|--------|
| Run `summer make:*` for plugin, model, migration, command, job, admin-controller | Stubs compile and pass `go vet` | `TestScaffoldAllArtifacts` generates all six into a copied hello workspace then `go build`/`go vet`; `TestMakeCommandsViaCLI` drives public cobra commands | ✓ |
| Resolve `vendor.plugin::group.key` for pl and en | Nested keys, CLDR plurals, `:name` substitution, fallback | Hello `LangFS` YAML + `TestHelloPluginResolvesLang`; `TestPluralSmoke` CLDR `one/few/many` and Laravel pipes | ✓ |
| Register mail template/layout and send | Dotted names, `-en` sibling, html/template + Goldmark, driver Send | Hello `HasMailTemplates`; memory render asserts subject/HTML/text; `TestSMTPMailpit` go-mail receipt in Mailpit | ✓ |
| Outcome | Phase gate green with Docker required | This session: `./scripts/check-phase4.sh` → `phase4 check passed`; forced `-count=1` on scaffold/i18n/mail including Mailpit PASS 1.88s | ✓ |
## Goal Achievement
### Observable Truths
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | `summer make:plugin`, `make:model`, `make:migration`, `make:command`, `make:job` and `make:admin-controller` each generate stubs that compile and pass `go vet` | ✓ VERIFIED | `cmd/summer/main.go` registers all six; `make:plugin` → `build.MakePlugin`; the others → `build.MakeModel`/`MakeMigration`/`MakeCommand`/`MakeJob`/`MakeAdminController`. Stubs render from embedded `internal/build/stubs/*.tmpl` through `go/format`. `TestScaffoldAllArtifacts` (this session 6.196s) writes Album+Widget models (`golem15_demo_albums`, timestamps, `--no-migration`), a separate `AddAlbumIndex` migration, `Purge` command, `Reindex` job (`Kind()`/`Work`, no River), `Albums` admin controller + `fields.yaml`/`columns.yaml`, then `go build ./...` and `go vet` on the copied workspace and plugin. Registry bytes are stable; handwritten `plugin.go` is untouched. `TestMakeCommandsViaCLI` executes `make:plugin`/`make:model --no-migration`/`make:job`/`make:command` through cobra. `TestModelsLeaf` fails `summer build` naming plugin ID, file, and sibling import. |
| 2 | A translation key `vendor.plugin::group.key` resolves for pl and en, including a CLDR plural form, loaded from per-plugin per-locale YAML files with parameter substitution | ✓ VERIFIED | Hello embeds `lang/en/lang.yaml` and `lang/pl/lang.yaml`. Keys are `golem15.hello::lang.greeting` / `lang.labels.pluginName`. `phrasebook.Activate` walks `HasLang` after Register and publishes `*phrasebook.Translator` before Boot. `Get`/`Choice` use `towel.Locale`; go-i18n v2.6.1 selects only the CLDR category via `IdentityParser`. This session: `go test ./phrasebook ./party -count=1` ok; `go -C examples/hello test ./... -count=1` ok. `TestHelloPluginResolvesLang`: pl=`Cześć`, en=`Hello`, nested labels, `pl-PL`→`pl`, `de`→configured `en`, missing key returns the raw key. `TestPluralSmoke`: pl albums 1/2/5/22 → one/few/many/few with `:count`; en `{0}`/`{1}`/`[2,*]` pipes; `:name`/`:Name`/`:NAME` → `hi alice Alice ALICE`; `{{.Count}}` stays literal. |
| 3 | A plugin registers a mail template and layout by dotted name with the per-locale suffix convention, and it renders via `html/template` through a driver interface (SMTP via go-mail) in a test send | ✓ VERIFIED | Hello registers `golem15.hello::mail.hello`, `golem15.hello::mail.hello-en`, layout alias `hello` → `golem15.hello::mail.layouts.hello`. `Send` takes the full dotted name and does no locale lookup. Render: `html/template` on Markdown → Goldmark HTML (`goldmark.New()`, no `html.WithUnsafe`) → substituted Markdown as text. Drivers: `memory`, `log`, `smtp` (`github.com/wneessen/go-mail` `DialAndSendWithContext`) behind `postcard.Driver`. `mail.driver` + `SUMMER_MAIL__DRIVER`. This session: `TestMailRenderSmoke` subject `Witaj Ada` / `Hello Ada`, HTML `Ada`, text `Witaj **Ada**.`; `TestSMTPMailpit` started `axllent/mailpit:v1.31.1`, sent via go-mail TLS `none`, Mailpit HTTP API asserted To/subject/HTML/text — PASS 1.88s. Missing template/layout fails `party: boot ` with the name. |
**Score:** 3/3 truths verified
Supporting plan truths (D-01…D-21: Winter directory shape, models-leaf check, registry.gen.go, gormigrate create-table, admin/job contracts, embedded templates, argument inference, HasLang load/flatten, both plural syntaxes, parameter case variants, fallback chain, en/en framework defaults, Winter mail headers, Goldmark safety, `-en` siblings, layout aliases, three drivers, Mailpit vs `-short`, backpack `Mailer.Send`, no-retry driver errors) all hold in code; they are not extra score rows.
### Required Artifacts
`gsd-sdk query verify.artifacts`: **19/19 passed** across the four plans.
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `internal/build/registry.go` | Deterministic per-plugin `registry.gen.go` | ✓ VERIFIED | Scan `Code generated by summer make`; atomic temp+rename; sorted models/migrations/commands/jobs/admin slices |
| `internal/build/leaf.go` | `models/` sibling-import enforcement | ✓ VERIFIED | `checkModelsLeaf` before `go build`; reports plugin ID, file, import |
| `internal/build/stubs/plugin.tmpl` | Winter-shaped plugin root, routes, leaves, go.mod | ✓ VERIFIED | `plugin.go`/`routes.go`/leaf `doc.go`; embeds `config`/`lang`/`views/mail`; generated accessors |
| `internal/build/stubs/artifacts.tmpl` | Model, migration, command, job, admin, YAML stubs | ✓ VERIFIED | GORM timestamps + `TableName`; gormigrate CREATE/DROP; `pact.Job` without River; `fields.yaml`/`columns.yaml` |
| `internal/build/stubs/registry.tmpl` | Generated-registry template | ✓ VERIFIED | `generatedModels`/`Migrations`/`Commands`/`Jobs`/`AdminControllers` |
| `cmd/summer/main.go` | Six make commands | ✓ VERIFIED | `make:plugin` plus five artifact commands; `--no-migration` bare flag |
| `pact/capabilities.go` | Job, admin, lang, mail contracts | ✓ VERIFIED | `Job`/`JobArgs`, `AdminController`, `HasLang`, `HasMailTemplates` |
| `phrasebook/loader.go` | Embedded YAML catalog load | ✓ VERIFIED | `lang//.yaml`; flatten; CLDR map vs nested map; goccy/go-yaml |
| `phrasebook/translator.go` | Get/Choice, CLDR, pipes, fallback, params | ✓ VERIFIED | `towel.Locale`; per-locale i18n.Bundle; `:name`/`:Name`/`:NAME`; missing-key log once |
| `party/registry.go` | HasLang + HasMailTemplates at Activate | ✓ VERIFIED | `phrasebook.Activate` then `postcard.Activate` after Register; `BootPlugin` before `Boot` |
| `postcard/templates.go` | Winter template/layout parse and render | ✓ VERIFIED | INI + `==`; `html/template` then Goldmark; layout `.Content` |
| `postcard/mailer.go` | App-scoped `Send(ctx, Message)` | ✓ VERIFIED | Full dotted name; optional subject override; one wrap, no retry |
| `postcard/drivers.go` | memory, log, SMTP behind `Driver` | ✓ VERIFIED | go-mail client; TLS mandatory/starttls/none; `FailDriver` |
| `postcard/assets/default.htm` | Neutral framework layout | ✓ VERIFIED | Header + text wrapper + HTML `content-body` |
| `internal/build/build_test.go` | Six-artifact compile/vet + leaf check | ✓ VERIFIED | `TestScaffoldAllArtifacts`, `TestModelsLeaf` |
| `phrasebook/translator_test.go` | Catalog, CLDR, pipe, substitution, fallback | ✓ VERIFIED | `TestTranslationSmoke`, `TestPluralSmoke`, `TestLocaleFallbackSmoke`, `TestCatalogBoundaries` |
| `postcard/mailpit_test.go` | Real SMTP receipt via Mailpit HTTP API | ✓ VERIFIED | This session PASS; `-short` skips only this test |
| `scripts/check-phase4.sh` | Repeatable root/hello/Mailpit/race gate | ✓ VERIFIED | Refuses missing Docker; no `-short`; this session `phase4 check passed` |
| `examples/hello/plugins/base/plugin.go` | HasLang + HasMailTemplates on the testbed plugin | ✓ VERIFIED | Embedded `lang/` and `views/mail/`; hello + hello-en + hello layout |
**Artifacts:** 19/19 verified
`internal/build/artifact.go` (Make* generators) and `internal/build/scaffold.go` (MakePlugin + embedded templates) are the shared scaffold entry the CLI calls; they are substantive and wired even though only `registry.go`/`leaf.go`/`scaffold` templates appear in plan 01 artifact paths.
### Key Link Verification
`gsd-sdk query verify.key-links` reported 2/12 verified. Same false negative as Phases 1–3: the checker greps target *filenames* inside source; Go imports package paths. Manual wiring:
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `cmd/summer/main.go` | `internal/build/scaffold.go` / `artifact.go` | make commands call `build.Make*` | ✓ WIRED | `MakePlugin` in `scaffold.go`; `MakeModel`/`Migration`/`Command`/`Job`/`AdminController` in `artifact.go` (same package). `TestMakeCommandsViaCLI` executes cobra → those funcs. |
| `internal/build/artifact.go` | `internal/build/registry.go` | `finishArtifact` → `refreshRegistry` | ✓ WIRED | Every successful artifact write refreshes `registry.gen.go` then `go mod tidy`. Plan listed `scaffold.go`; refresh lives next to the Make* writers. `MakePlugin` seeds an empty registry from `registry.tmpl`. |
| `internal/build/build.go` | `internal/build/leaf.go` | `checkModelsLeaf` before `go build` | ✓ WIRED | `App` line 40 calls `checkModelsLeaf` then `exec.CommandContext(..., "go", "build", ...)`. |
| `examples/hello/plugins/base/plugin.go` | `pact.HasLang` | `LangFS` embedded assets | ✓ WIRED | Compile-time assert `_ pact.HasLang`; `//go:embed lang`. gsd-sdk also verified this link. |
| `party/registry.go` | `phrasebook` (`loader.go` via `Activate`) | ordered HasLang scan before Boot | ✓ WIRED | `phrasebook.Activate(app, ordered)` after all `Register`; `cat.Load(p.ID(), hl.LangFS())`. |
| `phrasebook/translator.go` | `towel/context.go` | `towel.Locale` request-context seam | ✓ WIRED | `localeFrom` → `towel.Locale(ctx)`; hello test uses `towel.WithLocale`. |
| `examples/hello/plugins/base/plugin.go` | `pact.HasMailTemplates` | embedded mail FS + names | ✓ WIRED | `MailTemplatesFS`/`MailTemplates`/`MailLayouts`; gsd-sdk verified this link. |
| `party/registry.go` | `postcard` (`mailer.go`) | `postcard.Activate` + `BootPlugin` then `Publish[Mailer]` | ✓ WIRED | Activate publishes Mailer; `BootPlugin` registers declared templates/layouts and fails named missing files. |
| `postcard/mailer.go` | `postcard/drivers.go` | `driver.Send` after render | ✓ WIRED | `m.catalog.render` then `validateRendered` then `m.driver.Send`. |
| `scripts/check-phase4.sh` | `postcard/mailpit_test.go` | full `go test ./...` without `-short` | ✓ WIRED | Script runs `go test ./...` (includes `TestSMTPMailpit`) after refusing missing Docker. |
| `internal/build/build_test.go` | `internal/build/scaffold.go` | copied hello app drives public Make/Build API | ✓ WIRED | `copyHelloApp` → `MakePlugin`/`MakeModel`/… → `AddPlugin` → `App`. |
| `examples/hello/hello_test.go` | `party/registry.go` | Activate publishes translator and mailer | ✓ WIRED | `party.Activate` then `Lookup[*phrasebook.Translator]` and `Lookup[postcard.Mailer]`. |
**Wiring:** 12/12 connections verified (manual)
### Data-Flow Trace (Level 4)
| Artifact | Data | Source | Produces real data | Status |
|----------|------|--------|--------------------|--------|
| `Make*` stubs | Go sources + YAML | Embedded `text/template` + `go/format` | Yes — this session `TestScaffoldAllArtifacts` `go build`/`go vet` on generated plugin | ✓ FLOWING |
| `phrasebook.Translator` | `vendor.plugin::group.key` strings | Per-plugin `lang//.yaml` | Yes — hello embedded en/pl; CLDR few/many from go-i18n category selection + YAML maps | ✓ FLOWING |
| `postcard.Mailer.Send` | subject, HTML, text | `html/template` + Goldmark + layout wrappers | Yes — memory driver stores rendered parts; Mailpit HTTP API observed the SMTP payload | ✓ FLOWING |
| SMTP driver | RFC 5322 message | go-mail `DialAndSendWithContext` | Yes — this session Mailpit `To=ada@example.test`, subject `Witaj Ada`, HTML `Ada` | ✓ FLOWING |
## Requirements Coverage
PLAN frontmatter IDs: CLI-02 (01, 04), I18N-01 (02, 04), I18N-03 (03, 04). REQUIREMENTS.md maps the same three to Phase 4. No Phase 4 orphans. Every ID is accounted for.
I18N-02 (per-request `preferred_locale`) is Phase 7. CLI-03 (migrate rollback) is Phase 5. Admin schema depth is Phase 9. River adaptation of `pact.Job` is Phase 11. Those are later-phase requirements, not Phase 4 gaps.
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| CLI-02 | 01, 04 | Scaffolding commands generate a plugin, model, migration, command, job and admin controller with stubs that compile | ✓ SATISFIED | All six commands exist; generated plugin with every artifact type `go build`/`go vet` this session; CLI path proven for plugin/model/job/command |
| I18N-01 | 02, 04 | Translation keys use `vendor.plugin::group.key`, load from per-plugin per-locale YAML, parameters and CLDR plurals (go-i18n) for pl and en | ✓ SATISFIED | Hello YAML + phrasebook Get/Choice; Polish CLDR map and English pipes; `:name` case variants; go-i18n used only for category labels |
| I18N-03 | 03, 04 | Plugins register mail templates and layouts by dotted name with per-locale suffix, rendered with html/template, sent through a driver interface (SMTP via go-mail) | ✓ SATISFIED | `hello` / `hello-en` registration; memory + SMTP drivers; Mailpit receipt this session |
**Coverage:** 3/3 requirements satisfied
### Decision Coverage
All trackable CONTEXT.md decisions are honored by shipped artifacts. `gsd-sdk query check.decision-coverage-verify`: **21/21 honored**, `not_honored: []`. Lang D-01–D-05, mail D-06–D-09 and D-18–D-21, scaffolding D-10–D-17.
## Behavioral Verification
| Check | Result | Detail |
|-------|--------|--------|
| `./scripts/check-phase4.sh` | ✓ | This session: docker available; root `go vet`/`go test ./...`; hello vet/test; `-race` `./internal/build ./phrasebook ./postcard`; printed `phase4 check passed` (~14s; some packages cached from the implementation session) |
| `go test ./internal/build ./cmd/summer -run 'TestScaffold…\|TestModelsLeaf\|TestMakeCommandsViaCLI\|TestToolCommandNames' -count=1` | ✓ | internal/build 6.196s; cmd/summer 0.262s |
| `go test ./phrasebook ./party -count=1` | ✓ | phrasebook 0.007s; party 0.009s |
| `go -C examples/hello test ./... -count=1` | ✓ | 0.595s including `TestHelloPluginResolvesLang` and `TestHelloPluginSendsMail` |
| `go test ./postcard -count=1` | ✓ | 1.913s including Mailpit |
| `go test ./postcard -run TestSMTPMailpit -count=1 -v` | ✓ | Container `axllent/mailpit:v1.31.1` created, SMTP send, HTTP API receipt, PASS 1.88s |
| `go vet ./...` and `go -C examples/hello vet ./...` | ✓ | Clean this session |
| `go test ./postcard -run TestSMTPMailpit -short` skip-only-Mailpit | ℹ not re-run | Code: `if testing.Short() { t.Skip(...) }` then `t.Fatalf` on testcontainers error — Docker absence fails the full test, matching D-19. Gate does not pass `-short`. |
## Anti-Patterns Found
No `FIXME` / `XXX` / `TBD` / `HACK` / `coming soon` / `not implemented` in `internal/build`, `phrasebook`, `postcard`, `cmd/summer`, `pact`, or hello plugin sources.
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `internal/build/leaf.go` | 22–80 | Models leaf check is prefix-only, non-recursive, skips plugins outside the app root | ⚠️ Warning | Advisory WR-01 from `04-REVIEW.md`. Happy-path sibling import in `models/*.go` is rejected (`TestModelsLeaf`). Nested packages / aliased replace paths are bypassable. Does not break SC1: generated stubs still compile and the documented check fires on the intended shape. |
| `internal/build/registry.go` | 80–111 | Concurrent `make:*` can lose registry entries (no flock) | ⚠️ Warning | Advisory WR-02. Single-writer `TestScaffoldAllArtifacts` is stable. Agent-parallel `make:*` is a real race, not a missing feature. |
| `phrasebook/loader.go` | 243–263 | Catalog keys keep directory spelling (`pt_BR` vs `pt-BR`) | ⚠️ Warning | Advisory WR-03. D-04 `pl-PL`→`pl` works because files live under `pl` (hello + tests). Winter `pt_BR` trees are not a Phase 4 success criterion. |
| `postcard/templates.go` | 336–409 | `Vars` can carry `template.HTML`; regex is not a sanitizer | ⚠️ Warning | Advisory WR-04. String Vars cannot drop raw `