Six plans (tracer generator, site UX and checkers, content A, content B,
acme/blog walkthrough, unit tests). SC4/DOCS-04 narrowed to docs/ pages per
D-18; README Go fence conversion logged as a todo.
Per D-10 and DOCS-07, docs/setup/porting-a-plugin.md takes a WinterCMS `acme/blog` plugin (Plugin.php, a Post model, version.yaml updates, routes.php, a backend Posts controller with fields.yaml and columns.yaml, an artisan command) to a compiled SummerCMS plugin, showing the WinterCMS side as php/yaml fences and the SummerCMS side only as src= copies of docs/examples/blog.
Per D-16, docs/examples/blog is an ordinary package tree of the root module (import path git.golem15.com/golem15/summercms/docs/examples/blog), with no go.mod of its own, covered by root `go vet ./...` and `go test ./...`.
Per D-07, every Go fence and every YAML fence on the walkthrough page carries src= into docs/examples/blog and matches the source byte for byte; every referenced sub-package directory has _test.go files that exercise it.
The -short-safe tests activate the plugin through party and backpack, assert its routes are registered with surf, run the console command with bonfire into a buffer, and check the admin controller declaration; the Docker tests migrate up on an ICU pl-PL database, create, list and publish posts through the model, the route handler and the command, then roll back the last migration and confirm the column is gone.
TestScaffoldLayout runs build.MakePlugin, MakeModel, MakeAdminController, MakeCommand and MakeMigration for acme.blog into a copy of examples/hello and asserts that the relative file set of docs/examples/blog (without _test.go files, with 14-digit migration timestamps normalised) equals the scaffolder's output (without go.mod and go.sum).
The walkthrough's writes go through an explicit fill allow-list (lagoon.Fill) and the admin controller declares a permission; the route handler uses parameterised queries only.
Per D-11, the plugin, its tests and the page use only neutral names (acme, blog).
The walkthrough page lists the exact `summer make:*` commands that produced the layout, and the command checker accepts every `summer` and `./bin/<app>` token on it (blog:publish is collected from the docs/examples command literal).
Scaffolder oddities found while porting (for example same-second migration ordering by file name, or the generated-code header on files the developer edits) are described on the page as they are and logged under .planning/todos/pending/, without changing internal/build.
statement
verification
A WinterCMS developer can follow the walkthrough from top to bottom and end with the same plugin (manual review).
backstop
requirement_id
category
status
verification
resolution
reason
statement
DOCS-07
safety
resolved
judgment
Model writes use lagoon.Fill with an allow-list, the admin controller declares a permission, and the handler binds parameters.
Developers copy walkthrough code into production plugins; an unsafe pattern in the canonical example spreads to every port.
The walkthrough must not demonstrate a write path without an explicit fill allow-list or an admin controller without a declared permission.
path
provides
contains
docs/examples/blog/plugin.go
the acme.blog plugin
acme.blog
path
provides
contains
docs/examples/blog/scaffold_layout_test.go
TestScaffoldLayout pinning the walkthrough to the scaffolder
MakeAdminController
path
provides
contains
docs/examples/blog/postgres_test.go
Docker-backed migrate, CRUD, route, command and rollback tests
ICU_LOCALE
path
provides
contains
docs/setup/porting-a-plugin.md
the porting walkthrough page
src=docs/examples/blog/
from
to
via
pattern
docs/setup/porting-a-plugin.md
docs/examples/blog/plugin.go
go fence src= references kept in sync by the snippet check
src=docs/examples/blog/plugin.go
from
to
via
pattern
docs/examples/blog/scaffold_layout_test.go
internal/build/artifact.go
calls build.Make* into a temp app and compares file sets
build.Make
from
to
via
pattern
docs/examples/blog/registry.gen.go
docs/examples/blog/models/post.go
generated accessors list models, migrations, commands and admin controllers
generatedModels
Build the `acme/blog` porting walkthrough (D-10, D-16, DOCS-07): a real, compiled and tested SummerCMS plugin under `docs/examples/blog` with a model, two migrations, a route, an admin controller with its YAML, a console command and a language file, plus `docs/setup/porting-a-plugin.md` that walks a WinterCMS developer through it using only `src=` copies. A scaffold-layout test pins the tree to what `summer make:*` really emits.
Purpose: success criterion 5's walkthrough, verified under criterion 4.
Output: docs/examples/blog/** (package tree, tests, YAML), the walkthrough page, links from the concept map and index.
Scaffolder facts (probed at planning time by running the tool in a scratch copy of examples/hello): make:plugin acme.blog, make:model acme.blog Post, make:admin-controller acme.blog Posts, make:command acme.blog Publish and make:migration acme.blog AddPublishedAt produce, relative to the plugin directory: plugin.go, routes.go, registry.gen.go, go.mod, go.sum, classes/doc.go, config/config.yaml, console/doc.go, console/publish.go, controllers/doc.go, controllers/posts.go, controllers/posts/config_form.yaml, controllers/posts/config_list.yaml, jobs/doc.go, lang/en/lang.yaml, middleware/doc.go, models/doc.go, models/post.go, models/posts/fields.yaml, models/posts/columns.yaml, updates/doc.go, updates/<14-digit timestamp>_create_acme_blog_posts.go, updates/<timestamp>_add_published_at.go, views/mail/welcome.htm. The admin YAML sits under the controller's snake name (models/posts/), and the generated admin controller names the model Posts. Two migrations created in the same second are listed in registry.gen.go in file-name order (add_published_at before create_...). TestScaffoldLayout is the source of truth if the scaffolder output differs at execution time.
Authoring rules for the page are those of plans 11.1-03 and 11.1-04 (strict frontmatter, ASCII headings, pkg.Ident spans, src= fences filled by summer docs:sync, neutral names). Stage only each task's files.
Task 1: Tracer: the blog plugin activates and serves its posts route, and the walkthrough page shows it from verified source
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/classes/doc.go, docs/examples/blog/config/config.yaml, docs/examples/blog/console/doc.go, docs/examples/blog/controllers/doc.go, docs/examples/blog/jobs/doc.go, docs/examples/blog/lang/en/lang.yaml, docs/examples/blog/middleware/doc.go, docs/examples/blog/models/doc.go, docs/examples/blog/models/post.go, docs/examples/blog/models/post_test.go, docs/examples/blog/updates/doc.go, docs/examples/blog/updates/20260101000000_create_acme_blog_posts.go, docs/examples/blog/updates/updates_test.go, docs/examples/blog/views/mail/welcome.htm, docs/setup/porting-a-plugin.md, cmd/summer/docs_test.go
- internal/build/scaffold.go and internal/build/stubs/*.tmpl (plugin.go, routes.go, registry and doc.go shapes)
- internal/build/artifact.go (MakeModel and migration stubs)
- cmd/summer/main_test.go copyHelloApp and TestMakeCommandsViaCLI (how to run the scaffolder against a copy of examples/hello)
- examples/hello/plugins/greeter/plugin.go (a hand-finished plugin)
- modules/surf/README.md (routes, pact.Router, BuildRouter or Assemble for tests), modules/lagoon/README.md (Fill, Paginate), modules/wire/README.md
- docs/setup/coming-from-wintercms.md (tone and PHP fence style)
Per D-10 and D-16, start from real scaffolder output so the layout is honest.
Generate the skeleton: build the tool (go build -o <tmp>/summer ./cmd/summer), copy examples/hello into a temp dir with its framework replace pointed at the repository root (as copyHelloApp does), run make:plugin acme.blog and make:model acme.blog Post there, then copy plugins/blog/ into docs/examples/blog/ without go.mod and go.sum. Rename the migration to the fixed timestamp 20260101000000_create_acme_blog_posts.go (and its gormigrate ID to match) and rewrite every import path to git.golem15.com/golem15/summercms/docs/examples/blog/.... Keep the generated-code header lines as the scaffolder writes them.
Finish the model and migration: models.Post gains Title, Slug (unique) and Body; the publication timestamp arrives in Task 2 with the second migration. The create migration creates acme_blog_posts with those columns. Add a models.Post fill allow-list (title, slug, body) used through lagoon.Fill.
Route: in routes.go, GET /api/blog/posts lists posts newest first as JSON through lagoon.Paginate and wire.WriteJSON, using parameterised queries only and taking the *gorm.DB from the app the way framework plugins do (read lagoon's README). Declare it through pact.HasRoutes.
Tests (-short-safe, blog_test.go in package blog_test): TestPluginActivates activates the plugin through party/backpack and checks ID and capabilities; TestRoutesRegistered builds the router with surf and asserts the GET /api/blog/posts route exists without touching a database. models/post_test.go: TestPostTableAndFill (TableName and that lagoon.Fill with the allow-list drops an id key). updates/updates_test.go: TestMigrationIDs (IDs start with their 14-digit timestamps and are in ascending order in registry.gen.go's list).
Page docs/setup/porting-a-plugin.md (title "Porting a plugin", section setup, order 50): intro (what we port and why a neutral acme/blog), then sections "Plugin registration" (a php fence of Plugin.php and a go src=docs/examples/blog/plugin.go#Plugin style fence), "The Post model" (php fence of models/Post.php, go src=docs/examples/blog/models/post.go#Post), "Migrations" (yaml fence of WinterCMS version.yaml, go src= of the create migration), "Routes" (php fence of routes.php, go src= of the route declaration and handler). Fill all src= fences with go run ./cmd/summer docs:sync.
Append setup/porting-a-plugin to TestDocsRequiredPages.
go vet ./... && go test -short ./docs/examples/... -count=1 -v && go test ./cmd/summer -run '^(TestDocsTree|TestDocsRequiredPages)$' -count=1
<fails_when>non-zero exit, a "--- FAIL" line, or "no tests to run"</fails_when>
<acceptance_criteria>
test ! -e docs/examples/blog/go.mod (in-root package, D-16).
go list ./docs/examples/blog/... prints git.golem15.com/golem15/summercms/docs/examples/blog among its lines.
go test -short ./docs/examples/blog -run '^(TestPluginActivates|TestRoutesRegistered)$' -count=1 -v prints two --- PASS lines.
grep -c 'src=docs/examples/blog/' docs/setup/porting-a-plugin.md prints at least 4.
grep -n 'lagoon.Fill' docs/examples/blog/routes.go docs/examples/blog/models/post.go docs/examples/blog/models/post_test.go finds a match.
go run ./cmd/summer docs:build --check exits 0.
</acceptance_criteria>
A compiled acme.blog plugin with a model, a migration and a route lives in the root module, its fast tests pass, and the walkthrough page shows it through verified src= copies.
Task 2: The walkthrough adds the admin controller, the console command and a second migration, and proves them against Postgres
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/models/posts/fields.yaml, docs/examples/blog/models/posts/columns.yaml, docs/examples/blog/models/post.go, docs/examples/blog/console/publish.go, docs/examples/blog/console/publish_test.go, docs/examples/blog/updates/20260101000100_add_published_at.go, docs/examples/blog/registry.gen.go, docs/examples/blog/lang/en/lang.yaml, docs/examples/blog/postgres_test.go, docs/setup/porting-a-plugin.md
- internal/build/artifact.go MakeAdminController, MakeCommand, MakeMigration stubs
- modules/cabana/README.md and modules/pact/README.md (AdminController, AdminPermissioned, config_form/config_list keys, fields.yaml and columns.yaml)
- modules/bonfire/README.md (Command, Arg, Output)
- modules/lagoon/postgres_test.go (TestMain behaviour, dedicatedDB with the ICU pl-PL template) and modules/lagoon/README.md (Migrate, RollbackLast)
- docs/examples/blog (Task 1 state)
1. Scaffold the rest the same way as Task 1 (`make:admin-controller acme.blog Posts`, `make:command acme.blog Publish`, `make:migration acme.blog AddPublishedAt` in the temp app), copy the new files in, rename the migration to `20260101000100_add_published_at.go` (ID to match) and update `registry.gen.go` so migrations are listed create first, then add_published_at.
2. Finish them: the admin controller points at the `Post` model, declares a permission (for example `acme.blog.access_posts`) and uses config_form.yaml and config_list.yaml; `models/posts/fields.yaml` has title, slug, body and published_at fields and `columns.yaml` lists title, slug and published_at, all in WinterCMS YAML syntax. The migration adds `published_at TIMESTAMPTZ NULL` and its Rollback drops it; `models.Post` gains `PublishedAt *time.Time`. `console/publish.go` becomes `blog:publish` with a required `slug` argument that sets `published_at` on the matching post (parameterised) and prints `published `, returning an error for an unknown slug. `lang/en/lang.yaml` carries the plugin name, the permission label and the field labels, referenced from the YAML.
3. Tests: `controllers/posts_test.go` (`TestPostsControllerDeclaration`: ID, ConfigDir, permission; and compile the admin YAML through cabana if it can do so without a database, otherwise assert the YAML parses and move the compile check into the Docker test). `console/publish_test.go` (`TestPublishCommandShape`: name, argument, `bonfire.Call` with no slug returns an error). `postgres_test.go` (package `blog_test`): a `TestMain` that starts a testcontainers Postgres and creates a database with `TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'` (lagoon's idiom), skipping the container only under `testing.Short()` and failing when Docker is unavailable in a full run; `TestMigrateUpAndRollback` (lagoon.Migrate with the plugin, columns exist, `lagoon.RollbackLast` removes `published_at`, Migrate again), `TestPostsRouteAgainstDatabase` (seed through `lagoon.Fill`, GET /api/blog/posts through httptest returns them), `TestPublishCommandAgainstDatabase` (`bonfire.Call` of `blog:publish` sets `published_at` and prints `published `).
4. Page: add sections "Admin controller" (`php` fence of `controllers/Posts.php`, `go src=` of the controller, `yaml src=docs/examples/blog/controllers/posts/config_form.yaml`, `config_list.yaml`, `models/posts/fields.yaml`, `columns.yaml`), "Console command" (`php` fence of an artisan command, `go src=` of `console/publish.go`, a `sh` fence running `./bin/acme blog:publish hello-world`), "Adding a column" (the second migration, `summer migrate:rollback --plugin acme.blog`). Run `go run ./cmd/summer docs:sync`.
go vet ./... && go test ./docs/examples/... -count=1 -v
non-zero exit, a "--- FAIL" line, a "--- SKIP" line for a Postgres test in this full run, or "no tests to run"
go test ./cmd/summer -run '^(TestDocsTree|TestDocsRequiredPages)$' -count=1
non-zero exit or a "FAIL" line
- `go test ./docs/examples/blog -run '^(TestMigrateUpAndRollback|TestPostsRouteAgainstDatabase|TestPublishCommandAgainstDatabase)$' -count=1 -v` prints three `--- PASS` lines.
- `grep -c 'yaml src=docs/examples/blog/' docs/setup/porting-a-plugin.md` prints at least 4.
- `grep -n 'ICU_LOCALE' docs/examples/blog/postgres_test.go` finds a match.
- `grep -n '"blog:publish"' docs/examples/blog/console/publish.go` finds a match.
- `grep -En 'Permission|permission' docs/examples/blog/controllers/posts.go` finds a match.
- `go run ./cmd/summer docs:build --check` exits 0.
The walkthrough plugin has an admin controller with WinterCMS-style YAML, a console command and a reversible second migration, all exercised against a real Postgres, and the page shows them from verified source.
Task 3: The walkthrough is pinned to the scaffolder, lists the exact make commands, and is linked from the concept map and index
docs/examples/blog/scaffold_layout_test.go, docs/setup/porting-a-plugin.md, docs/setup/coming-from-wintercms.md, docs/index.md, cmd/summer/docs_test.go
- internal/build/scaffold.go MakePlugin and pluginLeaves, internal/build/artifact.go Make* signatures (`go doc ./internal/build`)
- cmd/summer/main_test.go copyHelloApp (replace rewrite for a temp app)
- docs/examples/blog (Task 2 state) and docs/setup/porting-a-plugin.md
- .planning/todos/pending/redacting-slog-handler.md (todo format)
1. `docs/examples/blog/scaffold_layout_test.go` (package `blog_test`, -short-safe, no network): `TestScaffoldLayout` copies examples/hello into `t.TempDir()` with its framework `replace` pointed at the repository root, calls `build.MakePlugin(ctx, dir, "acme.blog")`, `build.MakeModel(ctx, dir, "acme.blog", "Post", false)`, `build.MakeAdminController(ctx, dir, "acme.blog", "Posts")`, `build.MakeCommand(ctx, dir, "acme.blog", "Publish")` and `build.MakeMigration(ctx, dir, "acme.blog", "AddPublishedAt")`, collects the relative file set under `plugins/blog` without `go.mod` and `go.sum`, collects the relative file set of `docs/examples/blog` without `_test.go` files, replaces a leading 14-digit timestamp in `updates/` file names with a fixed token in both, and fails with the symmetric difference when the sets differ.
2. Page: add a "Scaffold it yourself" section with the exact commands in order (`summer make:plugin acme.blog`, `summer make:model acme.blog Post`, `summer make:migration acme.blog AddPublishedAt`, `summer make:admin-controller acme.blog Posts`, `summer make:command acme.blog Publish`, `summer plugin:add plugins/blog`, `summer build`) and what each writes, a note that the in-repository copy has no go.mod because it lives in the framework module (show the go.mod a real plugin gets as a `text` fence), and a closing "Checklist" of what changed from WinterCMS. Describe the scaffolder oddities as they are (the generated admin controller names the model after the controller; migrations created in the same second are ordered by file name; generated files carry a generated-code header that `summer make` uses to rebuild the accessors) and log each one that is a real gap as a todo under `.planning/todos/pending/` in a separate planning commit, without changing internal/build.
3. Link the walkthrough from `docs/setup/coming-from-wintercms.md` (intro paragraph) and `docs/index.md`.
4. Run `go run ./cmd/summer docs:sync` and `go run ./cmd/summer docs:build --check`; fix every problem.
go vet ./... && go test -short ./docs/examples/blog -run '^TestScaffoldLayout$' -count=1 -v && go test ./cmd/summer -run '^(TestDocsTree|TestDocsRequiredPages|TestDocsAIOutputsInSync)$' -count=1
non-zero exit, a "--- FAIL" line, or "no tests to run"
scripts/check-phase11.1.sh --docs && scripts/check-phase11.1.sh --forbidden
non-zero exit or a line starting "refuse:"
- `go test -short ./docs/examples/blog -run '^TestScaffoldLayout$' -count=1 -v` prints `--- PASS: TestScaffoldLayout`.
- `grep -c 'summer make:' docs/setup/porting-a-plugin.md` prints at least 5.
- `grep -n 'porting-a-plugin.md' docs/setup/coming-from-wintercms.md docs/index.md` finds a match in each file.
- `! grep -rniE 'fonoteka|p(l|ł)ytarium' docs` (no match).
- `git diff --name-only 9033d81 -- internal/build` prints nothing (scaffolder unchanged by this phase).
- `go run ./cmd/summer docs:build --check` exits 0.
The walkthrough's layout is pinned to the scaffolder by a test, the page lists the exact commands and the oddities are logged, and the concept map and index link it.
<threat_model>
Trust Boundaries
Boundary
Description
walkthrough code → developers' plugins
The canonical example is copied into real plugins
walkthrough tests → Postgres
Docker tests create and drop databases
STRIDE Threat Register
Threat ID
Category
Component
Severity
Disposition
Mitigation Plan
T-11.1-16
Tampering / Elevation of privilege
docs/examples/blog routes, models, controllers
medium
mitigate
Writes through lagoon.Fill with an allow-list (TestPostTableAndFill), admin controller declares a permission (TestPostsControllerDeclaration), handler and command use bound parameters only
T-11.1-17
Denial of service (test isolation)
docs/examples/blog/postgres_test.go
low
mitigate
testcontainers Postgres with a dedicated ICU database per run; never a developer or shared database
T-11.1-SC
Tampering
npm/pip/cargo/go installs
high
accept
This plan adds no module or package; testcontainers-go is already in go.mod
</threat_model>
- `go vet ./... && go test ./docs/examples/... -count=1` green with Docker; `go test -short ./docs/examples/...` green without.
- `go run ./cmd/summer docs:build --check` exits 0; `scripts/check-phase11.1.sh --docs --forbidden` pass.
<success_criteria>
SC5: the acme/blog porting walkthrough exists and its code (models, migrations, routes, admin controller, console command) is verified under criterion 4.
SC4: every Go and YAML snippet on the walkthrough page is a src= copy of code that compiles and runs under go test ./....
</success_criteria>
Artifacts this phase produces
Package tree git.golem15.com/golem15/summercms/docs/examples/blog with sub-packages models, updates, controllers, console, classes, jobs, middleware; symbols blog.Plugin, models.Post, updates.CreatePosts, updates.AddPublishedAt, controllers.PostsController, console.PublishCommand.
Application command declared by the example: blog:publish <slug>; route GET /api/blog/posts; permission acme.blog.access_posts (or the name chosen, recorded in the SUMMARY).
Page docs/setup/porting-a-plugin.md; links from docs/setup/coming-from-wintercms.md and docs/index.md; any scaffolder gap todos under .planning/todos/pending/.
Create `.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-05-SUMMARY.md` when done