docs(11.1-05): complete the acme/blog porting walkthrough plan

This commit is contained in:
Jakub Zych
2026-09-30 23:43:45 +02:00
parent 4e05450f46
commit 0223e39b2c

View File

@@ -0,0 +1,282 @@
---
phase: 11.1-summercms-documentation-for-humans-and-ai-agents
plan: 05
subsystem: docs
tags: [docs, wintercms, walkthrough, scaffolding, examples, lagoon, cabana, bonfire, surf]
requires:
- phase: 11.1-04
provides: "Backend, Database and Services pages the walkthrough links to; the src= snippet, command and identifier checkers"
- phase: 11.1-03
provides: "Coming from WinterCMS concept map, console scaffolding page, requiredPages"
provides:
- "docs/examples/blog: the acme.blog plugin as a package tree of the root module (blog.Plugin, models.Post, models.NewPost, updates.CreatePosts, updates.AddPublishedAt, controllers.PostsController, console.PublishCommand)"
- "Route GET /api/blog/posts (published posts, newest first, lagoon.Paginate), command blog:publish <slug>, permission acme.blog.access_posts"
- "Short tests TestPluginActivates, TestRoutesRegistered, TestPostTableAndFill, TestMigrationIDs, TestPostsControllerDeclaration, TestPublishCommandShape, TestScaffoldLayout"
- "Docker tests TestMigrateUpAndRollback, TestPostsRouteAgainstDatabase, TestPublishCommandAgainstDatabase, TestPublishCommandOpensDatabase on an ICU pl-PL database"
- "docs/setup/porting-a-plugin.md, linked from docs/setup/coming-from-wintercms.md and docs/index.md"
- "Three scaffolder gap todos under .planning/todos/pending/"
affects: [11.1-06]
estimate_ref: "tokens 110000, tasks 3, confidence low"
actuals:
tokens: 21070
tasks: 3
commits: 4
plan_head_before: 4e83d06025af7993233caf8fd7bf16ea7a2fcc83
plan_head_after: 4e05450f465c16a6a3c40b252bcdf612e091c390
tech-stack:
added: []
patterns:
- "Walkthrough code is real scaffolder output finished by hand, kept in the root module and pinned to the scaffolder by a file-set test"
- "A plugin reaches the database through the app it kept in Boot: Lookup[*gorm.DB] in handlers, withDB (published handle or lagoon.OpenFromApp for the command's duration) in commands"
- "Response DTOs built field by field (postJSON with wire.Time) rather than serialising the model"
- "Docker tests in a docs example package use their own testcontainers TestMain and one ICU pl-PL database per test"
key-files:
created:
- docs/examples/blog/plugin.go
- docs/examples/blog/routes.go
- docs/examples/blog/registry.gen.go
- docs/examples/blog/blog_test.go
- docs/examples/blog/postgres_test.go
- docs/examples/blog/scaffold_layout_test.go
- docs/examples/blog/models/post.go
- docs/examples/blog/models/post_test.go
- docs/examples/blog/models/posts/fields.yaml
- docs/examples/blog/models/posts/columns.yaml
- docs/examples/blog/updates/20260101000000_create_acme_blog_posts.go
- docs/examples/blog/updates/20260101000100_add_published_at.go
- docs/examples/blog/updates/updates_test.go
- docs/examples/blog/controllers/posts.go
- docs/examples/blog/controllers/posts_test.go
- docs/examples/blog/controllers/posts/config_form.yaml
- docs/examples/blog/controllers/posts/config_list.yaml
- docs/examples/blog/console/publish.go
- docs/examples/blog/console/publish_test.go
- docs/examples/blog/config/config.yaml
- docs/examples/blog/lang/en/lang.yaml
- docs/examples/blog/views/mail/welcome.htm
- docs/examples/blog/{classes,console,controllers,jobs,middleware,models,updates}/doc.go
- docs/setup/porting-a-plugin.md
- .planning/todos/pending/scaffold-same-second-migration-order.md
- .planning/todos/pending/scaffold-admin-controller-incomplete.md
- .planning/todos/pending/scaffold-generated-header-and-command-deps.md
modified:
- docs/setup/coming-from-wintercms.md
- docs/index.md
- docs/console/scaffolding.md
- cmd/summer/docs_test.go
key-decisions:
- "The permission is acme.blog.access_posts; the controller also implements pact.AdminRecordSource and the plugin pact.AdminAssets, pact.HasPermissions and pact.HasNavigation, because cabana refuses to start without AdminFS"
- "console.PublishCommand takes a withDB parameter: a generated zero-argument command cannot reach the app. The plugin returns it from Commands next to generatedCommands(), and registry.gen.go stays byte-identical to what the scaffolder regenerates for this tree (checked by re-running make: on a copy)"
- "fields.yaml has title, slug and body only: cabana forms have no date picker, so published_at is a datetime list column, is not fillable, and blog:publish sets it"
- "The posts route lists published posts only (published_at IS NOT NULL, newest first) through a postJSON DTO with wire.Time, and clamps page and per_page"
- "blog:publish uses COALESCE(published_at, NOW()) with a bound slug, so publishing twice keeps the first time and an unknown slug is an error"
- "Adding a column is a subsection of Migrations, before Routes, because the route already filters on published_at"
- "The scaffold-layout test calls the make functions in the page's order (plugin, model, migration, admin controller, command) and runs with GOPROXY=off"
patterns-established:
- "docs/examples/<plugin> is an in-root package tree; its layout is pinned by TestScaffoldLayout and its commands are collected by the docs command checker from bonfire.Command literals"
requirements-completed: [DOCS-04, DOCS-07]
coverage:
- id: D1
description: "The acme.blog plugin compiles in the root module, activates through party and backpack, and registers GET /api/blog/posts with surf"
requirement: DOCS-07
verification:
- kind: unit
ref: "docs/examples/blog/blog_test.go#TestPluginActivates"
status: pass
- kind: unit
ref: "docs/examples/blog/blog_test.go#TestRoutesRegistered"
status: pass
human_judgment: false
- id: D2
description: "Migrations go up on an ICU pl-PL database, the last one rolls back (published_at gone, table kept) and applies again"
requirement: DOCS-07
verification:
- kind: integration
ref: "docs/examples/blog/postgres_test.go#TestMigrateUpAndRollback"
status: pass
- kind: unit
ref: "docs/examples/blog/updates/updates_test.go#TestMigrationIDs"
status: pass
human_judgment: false
- id: D3
description: "The route serves published posts newest first with Laravel pagination meta and Carbon timestamps; writes go through the lagoon.Fill allow-list"
requirement: DOCS-07
verification:
- kind: integration
ref: "docs/examples/blog/postgres_test.go#TestPostsRouteAgainstDatabase"
status: pass
- kind: unit
ref: "docs/examples/blog/models/post_test.go#TestPostTableAndFill"
status: pass
human_judgment: false
- id: D4
description: "The admin controller declares acme.blog.access_posts, its YAML compiles through cabana without a database, and blog:publish runs with bonfire against a published and a self-opened database"
requirement: DOCS-07
verification:
- kind: unit
ref: "docs/examples/blog/controllers/posts_test.go#TestPostsControllerDeclaration"
status: pass
- kind: unit
ref: "docs/examples/blog/console/publish_test.go#TestPublishCommandShape"
status: pass
- kind: integration
ref: "docs/examples/blog/postgres_test.go#TestPublishCommandAgainstDatabase"
status: pass
- kind: integration
ref: "docs/examples/blog/postgres_test.go#TestPublishCommandOpensDatabase"
status: pass
human_judgment: false
- id: D5
description: "The walkthrough's file set equals what make:plugin, make:model, make:migration, make:admin-controller and make:command write for acme.blog"
requirement: DOCS-07
verification:
- kind: unit
ref: "docs/examples/blog/scaffold_layout_test.go#TestScaffoldLayout"
status: pass
human_judgment: false
- id: D6
description: "Every Go and YAML fence on porting-a-plugin.md is a src= copy of docs/examples/blog; the page lists the make commands and is linked from the concept map and index"
requirement: DOCS-04
verification:
- kind: other
ref: "go run ./cmd/summer docs:build --check (no problems); docs:sync all snippets up to date"
status: pass
- kind: unit
ref: "cmd/summer/docs_test.go#TestDocsRequiredPages"
status: pass
- kind: unit
ref: "cmd/summer/docs_test.go#TestDocsAIOutputsInSync"
status: pass
- kind: other
ref: "scripts/check-phase11.1.sh --docs and --forbidden"
status: pass
human_judgment: false
- id: D7
description: "A WinterCMS developer can follow the walkthrough from top to bottom and end with the same plugin"
verification: []
human_judgment: true
rationale: "Backstop truth in the plan: readability for a WinterCMS developer is a reader's judgment"
duration: 18min
completed: 2026-09-30
status: complete
---
# Phase 11.1 Plan 05: acme/blog porting walkthrough Summary
**A compiled `acme.blog` plugin under `docs/examples/blog`, built from real `summer make:*` output, with a Post model behind a fill allow-list, two reversible migrations, a published-posts API route, an admin controller behind `acme.blog.access_posts` and a `blog:publish` command, all tested against an ICU pl-PL Postgres. `docs/setup/porting-a-plugin.md` walks a WinterCMS developer through it with PHP next to `src=` copies, and a scaffold-layout test pins the tree to the scaffolder.**
## Performance
- **Duration:** about 18 min
- **Started:** 2026-09-30T21:24:56Z
- **Completed:** 2026-09-30T21:42:43Z
- **Tasks:** 3
- **Files modified:** 37
## Accomplishments
- **Tracer:** the scaffolder's `make:plugin` and `make:model` output, copied into the root module with fixed migration timestamps, became a plugin that activates, keeps the app in `Boot`, reads `acme.blog.per_page` from its embedded config and serves `GET /api/blog/posts` through `lagoon.Paginate` and `wire.WriteJSON`.
- **Admin, command, second migration:** `controllers.PostsController` serves `models.Post` behind `acme.blog.access_posts` with WinterCMS-style form and list YAML embedded through `pact.AdminAssets`; `blog:publish <slug>` publishes a post with a bound parameter; `updates.AddPublishedAt` adds and drops `published_at`. The route now lists published posts only.
- **Docker tier:** a package `TestMain` starts testcontainers Postgres (and fails the full run without Docker); each test gets its own `TEMPLATE template0 ... ICU_LOCALE 'pl-PL'` database. The tests migrate, roll back and re-apply, seed through `models.NewPost`, call the route through `surf.Assemble`, and run `blog:publish` both with the published handle and with one the command opens from `database.dsn`.
- **Scaffold pin:** `TestScaffoldLayout` runs the five make functions for `acme.blog` in a copy of `examples/hello` and diffs the file set against the walkthrough (mutation-checked with a planted file).
- **Page:** registration, model, migrations with an added column, routes, admin controller and console command sections, each PHP first; a "Scaffold it yourself" section with the exact commands and the go.mod a real plugin gets; "What the scaffolder leaves to you"; and a closing checklist. 20 `src=` fences (6 YAML), linked from the concept map and the index.
## Task Commits
1. **Task 1: Tracer, the plugin with its model, migration and posts route:** `41a3190` (feat)
2. **Task 2: Admin controller, publish command and published_at migration against Postgres:** `dd82b8a` (feat)
3. **Task 3: Scaffold-layout test, make commands on the page, concept map and index links:** `63290c6` (feat)
4. **Todos for the scaffolder gaps found while porting:** `4e05450` (docs)
**Plan metadata:** the docs(11.1-05) commit that adds this file
## Files Created/Modified
See `key-files` in the frontmatter.
## Decisions Made
See `key-decisions` in the frontmatter.
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] The scaffolded plugin cannot boot an admin controller**
- **Found during:** Task 2
- **Issue:** cabana refuses a plugin with admin controllers but no `AdminFS`, the generated controller has no record source or permission, and a `delete` toolbar button needs `showCheckboxes: true`. The model also needs `Rules` before the admin API saves.
- **Fix:** `plugin.go` embeds `controllers/*/*.yaml models/*/*.yaml` and implements `AdminFS`, `Permissions` and `Navigation`; the controller implements `NewRecord` and `RequiredPermissions`; `models.Post` has `Rules`; the list YAML sets `showCheckboxes`. Logged in `scaffold-admin-controller-incomplete.md`.
- **Commit:** dd82b8a, 4e05450
**2. [Rule 3 - Blocking] A generated command cannot reach the database**
- **Found during:** Task 2
- **Issue:** the registry only lists zero-argument command functions, and the generated main opens no database for plugin commands.
- **Fix:** `console.PublishCommand(withDB)` takes the plugin's `withDB` (published handle, else `lagoon.OpenFromApp` for the command's duration, as the framework's migrate commands do); `Plugin.Commands` appends it. `registry.gen.go` stays what the scaffolder regenerates. Logged in `scaffold-generated-header-and-command-deps.md`.
- **Commit:** dd82b8a, 4e05450
**3. [Rule 1 - Accuracy] published_at is not a form field**
- **Found during:** Task 2
- **Issue:** the plan asked for a `published_at` field in `fields.yaml`, but cabana forms have no date type (docs/backend/forms.md lists the supported types).
- **Fix:** the form has title, slug and body; `published_at` is a `datetime` list column and is set by `blog:publish`. The page explains why.
- **Commit:** dd82b8a
**4. [Rule 1 - Accuracy] docs/console/scaffolding.md claimed same-second migrations get consecutive timestamps**
- **Found during:** Task 3 (probe: `make:model` then `make:migration` in one second gave both files one timestamp, and `registry.gen.go` listed the ALTER migration first)
- **Fix:** the sentence now says only same-name migrations move to the next second; the porting page tells the developer to check `updates/`. `internal/build` is unchanged; the bug is logged in `scaffold-same-second-migration-order.md`.
- **Files modified:** docs/console/scaffolding.md (beyond the plan's file list)
- **Commit:** 63290c6, 4e05450
**5. [Rule 2 - Safety] The route lists published posts only and answers through a DTO**
- **Found during:** Task 2
- **Issue:** a public list of every post would expose drafts, and serialising the model would leak any column added later.
- **Fix:** `WHERE published_at IS NOT NULL`, a `postJSON` type with `wire.Time`, clamped `page` and `per_page`; `TestPostsRouteAgainstDatabase` asserts the draft is absent, `created_at` is not leaked and `per_page` is clamped.
- **Commit:** dd82b8a
### Other additions beyond the plan
- `models.NewPost`, the Go form of `Post::make($input)`, gives the walkthrough a write path through `lagoon.Fill` that the page shows and the Docker tests use to seed.
- `TestPublishCommandOpensDatabase` covers the `lagoon.OpenFromApp` branch of `withDB`, which is how the command runs from `./bin/acme`.
- The migrations section is ordered before the routes section, and "Adding a column" is its subsection, because the route filters on `published_at`.
**Total deviations:** 5 auto-fixed (2 blocking, 2 accuracy, 1 safety). **Impact:** no framework code changed; three scaffolder gaps are logged as todos.
## Issues Encountered
- A probe command in the scratch directory stalled on an interactive `rm` alias; it was stopped and the scratch files removed. No repository file was affected.
## Verification
- `go vet ./...` clean; `go test -short ./...` green.
- Full `go test ./... -count=1` with Docker: all 35 packages with tests pass, no FAIL lines; every walkthrough Docker test reports PASS, not SKIP.
- `go run ./cmd/summer docs:build --check`: no problems; `docs:sync`: all snippets up to date.
- `scripts/check-phase11.1.sh --docs`, `--forbidden` and `--deps`, `scripts/check-phase10.sh --hygiene` and `scripts/check-phase11.sh --hygiene` pass.
- Acceptance greps: no `docs/examples/blog/go.mod`; 20 `src=docs/examples/blog/` fences (6 YAML); 16 `summer make:` mentions; `lagoon.Fill` in models/post.go and post_test.go; `ICU_LOCALE` in postgres_test.go; `"blog:publish"` in console/publish.go; `RequiredPermissions` in controllers/posts.go; `porting-a-plugin.md` linked from coming-from-wintercms.md and index.md; no forbidden names under docs; `git diff --name-only 9033d81 -- internal/build` is empty.
## Known Stubs
None. `views/mail/welcome.htm` is the scaffolder's placeholder template, kept so the layout matches `make:plugin`; `MailTemplates` returns nil, so it is never registered or sent.
## Threat Flags
None. The example adds a public read-only route and an admin controller behind a permission; both are covered by the plan's threat register (T-11.1-16, T-11.1-17).
## User Setup Required
None.
## Next Phase Readiness
Plan 11.1-06 (unit tests last) can cover the walkthrough packages as they are; every sub-package with code already has a `_test.go`. The three scaffolder todos are ready for a code phase that owns `internal/build`.
## Self-Check: PASSED
All listed files exist. Commits 41a3190, dd82b8a, 63290c6 and 4e05450 are in the log.