--- 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 , 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/ 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 ` 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.