--- phase: 11.1-summercms-documentation-for-humans-and-ai-agents plan: 05 type: execute wave: 5 depends_on: ["11.1-04"] files_modified: - 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/classes/doc.go - docs/examples/blog/config/config.yaml - docs/examples/blog/console/doc.go - docs/examples/blog/console/publish.go - docs/examples/blog/console/publish_test.go - docs/examples/blog/controllers/doc.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/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/models/posts/fields.yaml - docs/examples/blog/models/posts/columns.yaml - docs/examples/blog/updates/doc.go - 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/views/mail/welcome.htm - docs/setup/porting-a-plugin.md - docs/setup/coming-from-wintercms.md - docs/index.md - cmd/summer/docs_test.go autonomous: true requirements: [DOCS-04, DOCS-07] assumption_delta_decision: no-change user_setup: [] estimate: tokens: 110000 raw_tokens: 110000 tasks: 3 confidence: low must_haves: truths: - "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/` 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: "A WinterCMS developer can follow the walkthrough from top to bottom and end with the same plugin (manual review)." verification: backstop prohibitions: - requirement_id: DOCS-07 category: safety status: resolved verification: judgment resolution: "Model writes use lagoon.Fill with an allow-list, the admin controller declares a permission, and the handler binds parameters." reason: "Developers copy walkthrough code into production plugins; an unsafe pattern in the canonical example spreads to every port." statement: "The walkthrough must not demonstrate a write path without an explicit fill allow-list or an admin controller without a declared permission." artifacts: - path: "docs/examples/blog/plugin.go" provides: "the acme.blog plugin" contains: "acme.blog" - path: "docs/examples/blog/scaffold_layout_test.go" provides: "TestScaffoldLayout pinning the walkthrough to the scaffolder" contains: "MakeAdminController" - path: "docs/examples/blog/postgres_test.go" provides: "Docker-backed migrate, CRUD, route, command and rollback tests" contains: "ICU_LOCALE" - path: "docs/setup/porting-a-plugin.md" provides: "the porting walkthrough page" contains: "src=docs/examples/blog/" key_links: - from: "docs/setup/porting-a-plugin.md" to: "docs/examples/blog/plugin.go" via: "go fence src= references kept in sync by the snippet check" pattern: "src=docs/examples/blog/plugin\\.go" - from: "docs/examples/blog/scaffold_layout_test.go" to: "internal/build/artifact.go" via: "calls build.Make* into a temp app and compares file sets" pattern: "build\\.Make" - from: "docs/examples/blog/registry.gen.go" to: "docs/examples/blog/models/post.go" via: "generated accessors list models, migrations, commands and admin controllers" pattern: "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. @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md @.planning/STATE.md @.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-CONTEXT.md @.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-RESEARCH.md @.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-04-SUMMARY.md @CLAUDE.md @internal/build/scaffold.go @internal/build/artifact.go @cmd/summer/main_test.go @examples/hello/plugins/greeter/plugin.go @modules/lagoon/postgres_test.go 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/_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. 1. Generate the skeleton: build the tool (`go build -o /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. 2. 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`. 3. 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`. 4. 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). 5. 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`. 6. 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 non-zero exit, a "--- FAIL" line, or "no tests to run" - `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. 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. ## 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 | - `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. - 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 ./...`. ## 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 `; route `GET /api/blog/posts`; permission `acme.blog.access_posts` (or the name chosen, recorded in the SUMMARY). - Tests: `TestPluginActivates`, `TestRoutesRegistered`, `TestPostTableAndFill`, `TestMigrationIDs`, `TestPostsControllerDeclaration`, `TestPublishCommandShape`, `TestMigrateUpAndRollback`, `TestPostsRouteAgainstDatabase`, `TestPublishCommandAgainstDatabase`, `TestScaffoldLayout`. - 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