75 KiB
Phase 11.1: SummerCMS documentation for humans and AI agents - Research
Researched: 2026-09-28
Domain: Static documentation generator in Go (goldmark + html/template), docs-as-code verification (compiled snippets, identifier/link checkers), llms.txt publishing
Confidence: HIGH for the in-repo mechanics (goldmark API, go/ast checker, vet behaviour, CLI naming: all probed this session). MEDIUM for the WinterCMS docs anatomy and the llms.txt convention (official pages fetched, but the seam rates web sources LOW). LOW where tagged [ASSUMED].
Summary
This phase adds no runtime framework behaviour. It adds a docs pipeline and content. The main risk is drift, not the technology: docs that name APIs that do not exist, snippets that no longer compile, and links that break. The design below makes every one of those a go test ./... failure, and it needs zero new dependencies. github.com/yuin/goldmark v1.8.6 is already a direct requirement in go.mod (postcard uses it for mail). The frontmatter parser is the in-tree goccy/go-yaml. Go syntax highlighting can use stdlib go/scanner. The identifier checker uses stdlib go/parser/go/ast. A prototype run this session checked all 742 package-qualified identifier spans in the 18 module READMEs and the root README with no misses.
The key decision (D-07) is include by reference, embedmd style. Each Go fence in the Markdown carries a source reference in its info string (```go src=modules/bonfire/example_test.go#ExampleNewRoot). The fence body is a verbatim copy of that source region, so the file reads well on the git host (D-01). A test fails when the copy drifts from the source, and summer docs:sync rewrites the copies. The referenced sources are real Go: Example* functions with // Output: in the module packages, which go test runs and whose names go vet checks against real identifiers (probed), plus the acme/blog walkthrough plugin as real packages under docs/examples/blog. The alternative, extracting fenced snippets and compiling them, was rejected. It needs wrapper templates, it cannot be refactored with gopls, and it reports errors against generated files.
For D-08 the recommendation is ingest, don't duplicate. The generator publishes every modules/*/README.md as a page in the "API reference" section, with a synthesized title and description and with links rewritten. The READMEs stay the single per-package reference, so they cannot drift from a second copy, and the identifier checker covers them too. That automates the manual go doc rule in CLAUDE.md.
Primary recommendation: Build internal/docsite (a stdlib + goldmark generator, embedded theme) behind summer docs:build, summer docs:serve and summer docs:sync. Keep every accuracy check (snippet sync, identifiers, links and anchors, CLI names, forbidden app names, page-tree/llms sync) as a plain Go test that runs over the real docs/ tree, so go test ./... is the gate.
<user_constraints>
User Constraints (from CONTEXT.md)
Locked Decisions
Source and build
- D-01: The source is Markdown with YAML frontmatter (title, description, section, order) under
summercms.go/docs/. The files read well on the git host without the site build. - D-02: A small Go generator renders the site: stdlib
html/templateplusgithub.com/yuin/goldmarkfor Markdown. The user approved goldmark in this discussion, which satisfies the CLAUDE.md dependency rule. Any further dependency (syntax highlighting such as chroma, a frontmatter parser beyond goccy/go-yaml) must be named in RESEARCH.md and confirmed at the plan-count checkpoint. - D-03: The generator is exposed through the
summerCLI (preferredsummer docs:buildandsummer docs:serve, final names subject to research against existing bonfire command naming). It produces a self-contained static output directory. No Node or npm toolchain. - D-04: The site layout follows wintercms.com/docs: a left sidebar grouped by section, an on-page table of contents, prev/next links, "edit this page" source links, client-side search over a generated JSON index (vanilla JS, no framework) and a dark mode. Research should record what the Winter docs actually offer and flag anything left out.
AI-friendly outputs
- D-05: The build emits
llms.txt(an index following the llms.txt convention) andllms-full.txt(every page concatenated in sidebar order) at the site root. - D-06: Every rendered page is also published as clean Markdown at a predictable URL (page URL plus
.md), without site chrome. - D-07: Code examples are verified. Go snippets shown in the docs are compiled and run as part of
go test ./..., either asExamplefunctions included into pages by reference or as snippet files extracted and built by a test. Research picks one mechanism. A doc build fails if a referenced snippet is missing.
Structure and content
- D-08: The sections mirror WinterCMS: Getting started/Setup, Architecture, Plugins, Backend (admin auth, forms, lists, relation manager, settings), Database (models, migrations, relations, casts, validation), Services (events, config, mail, i18n, jobs, realtime, search, storage, rate limiting, HTTP routing and auth groups), Console (CLI commands and scaffolding), and API reference. Module READMEs remain the per-package reference. The docs link to them or derive from them rather than duplicating them. Research decides which.
- D-09: A "Coming from WinterCMS" page maps Winter concepts to SummerCMS equivalents (for example Plugin.php to the Plugin interface,
fields.yamlto the schema pipeline, Eloquent to GORM plus lagoon, and so on). - D-10: A porting walkthrough takes a neutral sample WinterCMS plugin (
acme/blog) through to a compiled SummerCMS plugin: models, migrations, routes, admin controller and a scaffolding command. Its code is verified under D-07.
Accuracy rules (inherited from CLAUDE.md, applied to docs/)
- D-11: The docs never name a consuming application. Use "the application" or "host application" and neutral names such as
blogoracme. - D-12: Every identifier named in the docs must exist in the package. A checker verifies it (as
go doc ./modules/<name> <Identifier>does for READMEs) and runs in the test suite. Internal links and anchors are checked too. - D-13: From this phase on, a change to a module's exported API, config keys or CLI commands must update both the module README and the affected docs pages. Record this rule in CLAUDE.md's Documentation section as part of the phase.
Claude's Discretion
- Exact page list within each section, URL scheme, theme styling (reuse the admin SPA's design tokens if convenient), search index format, and the generator's package location (for example
modules/<beach-name>orinternal/docs, following the existing naming convention).
Deferred Ideas (OUT OF SCOPE)
- An agent skill or
AGENTS.md/CLAUDE.mdtemplate for host applications. It was offered and not selected for this phase. - Hosting and CI deployment of the site.
- Versioned docs (per framework release). </user_constraints>
<phase_requirements>
Phase Requirements (proposed IDs; none were assigned)
The roadmap lists Requirements: TBD. Proposed IDs, for adding to REQUIREMENTS.md in a new "Documentation (DOCS)" group and to the traceability table as Phase 11.1:
| ID | Description | Success criterion | Research support |
|---|---|---|---|
| DOCS-01 | docs/ holds Markdown pages with strict YAML frontmatter (title, description, section, order), grouped into the Winter-mirroring sections. Every framework module under modules/ is reachable from the sidebar through an API reference page ingested from its README. |
SC1 | §Page tree, §README strategy (D-08), TestContentTree |
| DOCS-02 | summer docs:build writes a self-contained static site (sidebar, on-page TOC, prev/next, edit-this-page link, client-side search, light/dark/system theme). summer docs:serve previews it on loopback. No Node toolchain, and no new dependency beyond goldmark unless approved. |
SC2 | §Generator, §Theme, §Search |
| DOCS-03 | The build emits llms.txt, llms-full.txt and a clean .md for every page, and a test asserts all three match the page tree. |
SC3 | §AI outputs, TestAIOutputsInSync |
| DOCS-04 | Every Go fence in the docs references compiled source by src=. A test fails on a missing or drifted snippet. Referenced Examples carry // Output: and run under go test ./.... |
SC4 | §Verified examples |
| DOCS-05 | go test ./... runs these checkers and they fail on stale names or broken links: identifiers (docs pages and module READMEs), internal links and anchors, summer/app CLI command names, and forbidden consuming-application names. |
SC4 (+D-11, D-12) | §Checkers |
| DOCS-06 | A "Coming from WinterCMS" page maps Winter concepts to SummerCMS equivalents. Every SummerCMS identifier on it is checker-verified. | SC5 | §Concept map draft |
| DOCS-07 | An acme/blog porting walkthrough covers models, migrations, routes, an admin controller and a console command. Its code is real packages under docs/examples/blog, verified by DOCS-04. |
SC5 | §Walkthrough |
| DOCS-08 | CLAUDE.md's Documentation section records the D-13 rule (API, config or CLI changes update the README and the affected docs pages) and names the automated checkers. | D-13 | §Hygiene gate |
| </phase_requirements> |
Project Constraints (from CLAUDE.md)
- Stdlib first (net/http, html/template, encoding/json). Add a dependency only when research or a phase decision names it. This research names none (goldmark is already approved and in
go.mod). go vet ./...andgo test ./...stay green at every commit.- Lean planning: few, large plans. Checkpoint the plan count before writing PLAN.md files. Unit tests are the last plan.
- No runtime plugin loading. (Not relevant here. The docs generator is tool code, not a plugin.)
- Commits: never add co-author tags, one logical change per commit, planning docs and code in separate commits.
- Documentation rules: README updates in the same change as API, config, CLI or dependency changes. Standard README structure, with a root-table row for a new module. READMEs never name a consuming application. Every README identifier must exist (
go doccheck). - Two repos: this phase writes to summercms.go only. Framework docs never name the consuming application (D-11).
- GSD workflow enforcement: edits happen inside GSD commands.
Architectural Responsibility Map
| Capability | Primary tier | Secondary tier | Rationale |
|---|---|---|---|
| Markdown source, frontmatter, snippet copies | Repo content (docs/) |
— | D-01: the source of truth, readable on the git host |
| Parse, render, TOC, heading IDs, link rewriting | Build tool (internal/docsite, invoked by the summer CLI) |
— | A tool-time concern like internal/build. Not a runtime framework API |
| Site chrome (sidebar, TOC, prev/next, theme toggle) | Static HTML/CSS (html/template output) | Browser JS (theme toggle, mobile nav) | Works with JS disabled, except search and the toggle |
| Search | Browser (vanilla JS over a JSON index) | Build tool (index generation) | D-04: client-side, no server |
| llms.txt, llms-full.txt, per-page .md | Build tool (static files) | — | D-05, D-06: plain files at predictable URLs |
| Snippet, identifier, link and CLI verification | Test suite (go test ./...) |
Build tool (docs:build fails on the same errors) |
SC4: tests are the gate. The build refuses broken input too |
| Verified example code | Module packages (example_test.go) and docs/examples/blog |
— | Real compiled Go that go vet and go test cover |
| Preview server | Build tool (docs:serve, loopback only) |
— | Local dev only. Hosting is deferred |
Standard Stack
Core (all already in go.mod, no approval needed)
| Library | Version | Purpose | Why standard |
|---|---|---|---|
github.com/yuin/goldmark |
v1.8.6 (proxy: published 2026-09-03) | CommonMark + GFM parse/render, AST for TOC, links, code spans and fences | Approved in D-02. Already a direct dependency: go.mod:26 reads github.com/yuin/goldmark v1.8.6 [VERIFIED: go.mod:26]. Already imported by postcard: "github.com/yuin/goldmark" / markdown = goldmark.New() [VERIFIED: modules/postcard/templates.go:14,29]. Its go.mod has no requirements [VERIFIED: proxy.golang.org goldmark v1.8.6 .mod] |
github.com/goccy/go-yaml |
v1.19.2 | Strict frontmatter and docs/site.yaml decoding |
go.mod:13 reads github.com/goccy/go-yaml v1.19.2 [VERIFIED: go.mod:13]. DisallowUnknownField() exists [VERIFIED: go-yaml option.go:65] and is already the tide idiom (yaml.NewDecoder(bytes.NewReader(raw), yaml.DisallowUnknownField()), modules/tide/manifest.go:96) |
stdlib html/template, embed, net/http, encoding/json |
Go 1.27 | Page chrome, embedded theme, docs:serve, search index |
Project rule |
stdlib go/parser, go/ast, go/token, go/doc, go/scanner, go/format |
Go 1.27 | Snippet region extraction, identifier index, Go highlighting | Stdlib. The prototype parsed all 18 modules, including Go 1.27 generic methods (func (b *Bus) Fire[T any](...)) |
github.com/fsnotify/fsnotify |
v1.10.1 | docs:serve rebuild on change (optional) |
Already direct (go.mod:10, used by internal/dev) [VERIFIED: go.mod:10] |
goldmark features to enable (all in core goldmark)
extension.GFM, which is exactlyLinkify,Table,StrikethroughandTaskList[VERIFIED: goldmark extension/gfm.go:11-18].parser.WithAutoHeadingID()[VERIFIED: parser/atx_heading.go:45-49], used with a customparser.IDspassed throughparser.WithIDs(ids)on the parse context [VERIFIED: parser/parser.go:81-87, 240-244]. The reason is under Pitfall 3.parser.WithASTTransformers(...)[VERIFIED: parser/parser.go:721] for link rewriting, callout (> [!NOTE]) detection and H1 stripping.- Keep the html renderer without
html.WithUnsafe()[VERIFIED: renderer/html/html.go:236]. Raw HTML in docs is not needed and stays escaped. ast.FencedCodeBlock.Language(source)returns the info string up to the first space [VERIFIED: goldmark ast/block.go:309-321]. So```go src=...has languagego, and the rest of the info string is available throughn.Info.
Alternatives considered (each needs explicit user approval; none recommended)
| Instead of | Could use | Tradeoff / verdict |
|---|---|---|
Stdlib go/scanner highlighter for go fences (plus a small line-regex highlighter for yaml/sh) |
github.com/alecthomas/chroma/v2 v2.27.0 (2026-06-17; deps dlclark/regexp2/v2) called from a small custom goldmark NodeRenderer |
Chroma highlights every language and ships themes. It costs 2 new modules and a lot of lexer code. Docs are almost entirely Go, YAML and shell. Not recommended. Ask at the checkpoint only if the user wants multi-language highlighting |
| — | github.com/yuin/goldmark-highlighting/v2 |
Reject. It has never been tagged: the only version is pseudo-version v2.0.0-20230729083705-37449abec8cc, and its go.mod pins chroma/v2 v2.2.0 from 2023 [VERIFIED: proxy.golang.org] |
Manual --- split + goccy/go-yaml |
github.com/yuin/goldmark-meta v1.1.0 (2022) |
Reject. Its go.mod requires gopkg.in/yaml.v2 v2.3.0 [VERIFIED: proxy.golang.org .mod], an unmaintained YAML line the stack research already warns against |
| AST walk for the TOC (~30 lines) | go.abhg.dev/goldmark/toc v0.12.0 |
Not needed. The headings already carry IDs in the AST |
Stdlib go/ast identifier index |
golang.org/x/tools/go/packages v0.50.0 |
Adds a heavy module for type-checked loading the checker does not need (0/742 misses with plain AST). Not needed |
| JSON search index + vanilla JS | lunr/pagefind/Algolia | Violates "no Node" or adds a service. Out |
Installation: none. go.mod does not change in this phase. The gate should assert that (see the hygiene gate).
Package Legitimacy Audit
gsd-tools package-legitimacy check supports only npm, pypi and crates, so it rejected --ecosystem go. Verification was done against proxy.golang.org directly.
| Package | Registry | Age | Source repo | Verdict | Disposition |
|---|---|---|---|---|---|
| github.com/yuin/goldmark v1.8.6 | proxy.golang.org | tagged 2026-09-03, project since 2019 | github.com/yuin/goldmark | OK (already a direct dependency, approved in D-02) | Approved, no change |
| github.com/goccy/go-yaml v1.19.2 | proxy.golang.org | already direct | github.com/goccy/go-yaml | OK (already in tree) | Approved, no change |
| github.com/alecthomas/chroma/v2 v2.27.0 | proxy.golang.org | tagged 2026-06-17 | github.com/alecthomas/chroma | OK on registry. Only an alternative | Not recommended. User approval required if chosen |
| github.com/yuin/goldmark-highlighting/v2 | proxy.golang.org | untagged pseudo-version (2023) | github.com/yuin/goldmark-highlighting | SUS (never tagged, stale pins) | REMOVED from recommendations |
| github.com/yuin/goldmark-meta v1.1.0 | proxy.golang.org | 2022 | github.com/yuin/goldmark-meta | OK on registry, but pulls gopkg.in/yaml.v2 |
REMOVED from recommendations |
Packages removed: goldmark-highlighting/v2, goldmark-meta. Packages flagged SUS: goldmark-highlighting/v2 (removed anyway).
Research question answers
Q1. wintercms.com/docs: tree, anatomy, tone, mapping
Sidebar (v1.2) [CITED: wintercms.com/docs/v1.2/docs/setup/installation, …/database/model]:
- Getting Started: Installation, Configuration, Upgrade Guide
- Architecture Concepts: Introduction, Using Composer, Developer Guide, Maintainer Guide
- Backend: Controllers & AJAX, Views & Partials, Widgets, Forms, Lists, Relations, Sorting records, Importing & Exporting, Users & Permissions, User Interface Guide
- Frontend: Themes, Developing Themes, Pages, Partials, Layouts, Content Blocks, Components, Media Manager, Markup Guide, Twig Docs
- Plugins: Registration, Version History, Building Components, Settings & Config, Localization, Task Scheduling, Extending Plugins, Replacement & Forking, Unit Testing
- AJAX Framework: Introduction, Event Handlers, Updating Partials, Data Attributes API, JavaScript API, Extra Features
- Snowboard: Introduction, Migration Guide, Serverside Event Handlers, AJAX Requests (JS API), AJAX Requests (Data Attributes API), Extra Features, Utilities, Plugin Development
- Database: Getting Started, Structure, Queries, Models, Relationships, File Attachments, Collections, Mutators, Serialization, Traits, Behaviors
- Services: Application, Asset Compilation, Behaviors, Cache, Collections, Errors & Logging, Filesystem / CDN, Forms & Html, Hashing & Encryption, Helpers, Image Resizing, Mail, Pagination, Parser, Queues, Router, Request & Input, Response & View, Session, Validation
- Console: Introduction, Setup & Maintenance, Plugin Management, Theme Management, Asset Compilation (+Mix, +Vite, +Node Utilities), Scaffolding, Utilities
- Events: Introduction, Frontend Events Timeline, Available Events
- Further Reading: Laravel Docs, PHP Docs (external)
Page anatomy [CITED: wintercms.com/docs/v1.2/docs/setup/installation]:
- A header with the logo and a version selector (v1.2 / Develop).
- Category tabs: Docs, API, Markup, UI.
- A "Search docs ⌘K" box.
- An "On this page" TOC.
- A "Next page → Section: Title" link at the bottom.
- An "Edit on GitHub" button.
- A light/dark/system theme toggle.
- A copyright footer.
The source Markdown uses > **NOTE:** callouts, fenced code with language tags, ASCII directory trees and two-column Key/Description tables. Tone: second person, present tense, imperative ("Define the $elevated property") [CITED: raw.githubusercontent.com/wintercms/docs/develop/plugin/registration.md].
What this phase includes and leaves out (D-04 flag):
| Winter feature | SummerCMS docs | Note |
|---|---|---|
| Sidebar grouped by section | Include | Sections from D-08 |
| "On this page" TOC | Include | H2 and H3 |
| Next/prev page | Include | Sidebar order, crossing section boundaries |
| Edit on GitHub | Include as "Edit this page" | The forge is Gitea (git.golem15.com serves Gitea markup, probed). Configure the URL as a template in docs/site.yaml |
| Search ⌘K | Include | ⌘K / Ctrl+K and /, JSON index |
| Light/dark/system toggle | Include | Tri-state, persisted in localStorage. Note that the admin SPA follows the system only (theme.ts:1-2 reads "the admin follows the system preference only, with no toggle"), so the docs toggle is new behaviour |
| Version selector | Leave out | Versioned docs are deferred |
| Docs/API/Markup/UI tabs | Leave out | Replaced by one tree with an "API reference" section |
| og-description div | Replaced | By the frontmatter description, rendered as <meta name="description"> and OG tags |
| (new) "View as Markdown" link per page | Add | Points at the page .md. Cheap and AI-friendly |
| (new) Copy button on code blocks | Optional | UI-SPEC decides |
Winter section → SummerCMS page → module(s):
| Winter section / page | SummerCMS | Module(s) | Disposition |
|---|---|---|---|
| Getting Started: Installation, Configuration | Setup: Introduction, Installation, Configuration | compass, lagoon (DB locale), cmd/summer | Port |
| Getting Started: Upgrade Guide | — | — | Omit (no releases, versioned docs deferred) |
| — | Setup: Coming from WinterCMS, Porting a plugin (walkthrough) | all | New (D-09, D-10) |
| Architecture: Introduction | Architecture: Introduction (single binary, compiled plugins, headless) | party, backpack | Port |
| Architecture: Using Composer | Architecture: Go modules and workspaces (go.work, replace, summer.yaml) |
internal/build | Differs |
| — | Architecture: Application lifecycle (Register/Boot, container, services), Request lifecycle (surf pipeline, towel context) | party, backpack, surf, towel | New |
| Architecture: Developer/Maintainer Guide | — | — | Omit (contributor process lives in CLAUDE.md) |
| Plugins: Registration | Plugins: Registration | pact, party | Port |
| Plugins: Version History | Plugins: Migrations and versions (gormigrate, per-plugin history) | lagoon | Differs |
| Plugins: Settings & Config | Plugins: Configuration and settings | compass, pact (pact.SettingsItem), cabana |
Port |
| Plugins: Localization | Plugins: Localization | phrasebook | Port |
| Plugins: Task Scheduling | Plugins: Task scheduling | Phase 11 scheduler, HasSchedule |
Port after Phase 11 |
| Plugins: Extending Plugins, Replacement & Forking | Plugins: Extending plugins (events, GORM callbacks, companion structs, optional plugins, services) | festival, lagoon, backpack | Port. Fold forking into it (replace directive) |
| Plugins: Building Components | — | — | "Differs" note on the Frontend page (headless: components become HTTP handlers) |
| Plugins: Unit Testing | Plugins: Testing (go test, testcontainers, parity replay) | tide | Port |
| Backend: Controllers & AJAX | Backend: Admin controllers (JSON admin API, hooks) | cabana, pact | Differs (no AJAX) |
| Backend: Forms / Lists / Relations | Backend: Forms, Lists and filters, Relation manager | cabana | Port |
| Backend: Users & Permissions | Backend: Admin users and permissions | cabana, bouncer, pact | Port |
| Backend: Views & Partials, Widgets | Backend: Partials, widgets and assets | cabana/boardwalk after Phase 10.1 | Port after 10.1 |
| Backend: UI Guide | Backend: The admin SPA | boardwalk | Differs (compiled SPA) |
| Backend: Sorting records, Import/Export | — | — | Omit. One line in "Coming from WinterCMS": not provided (FW-05 deferred) |
| Frontend (all 10 pages), AJAX Framework, Snowboard | One page: "Frontend and AJAX (not provided)" | — | "Not supported / differs" page, not omission. Winter users will look for it. It explains headless + JSON API + realtime |
| Database: Getting Started, Structure, Queries | Database: Connection, Migrations, Queries and pagination | lagoon | Port |
| Database: Models, Relationships, File Attachments | Database: Models, Relations, File attachments | lagoon, lagoon/attach | Port |
| Database: Mutators, Serialization, Traits | Database: Casts, Mass assignment and serialization, Validation | lagoon, wire | Port |
| Database: Collections, Behaviors | — | — | Omit (Go slices and composition). One line in the concept map |
| Services: Application, Router, Request & Input, Response & View, Validation, Mail, Pagination, Hashing & Encryption, Filesystem/CDN, Queues | Services: Container and services, Routing and auth groups, Rate limiting, HTTP responses, Mail, Hashing and encryption, File storage, Queues and jobs | backpack, surf, bouncer, wire, postcard, lagoon, Phase 11 jobs | Port |
| Events (own section) | Services: Events | festival | Port (D-08 puts events in Services) |
| — | Services: Authentication, OAuth server, Outbound HTTP, Realtime, Search, Localization in requests, API parity testing | bouncer, wristband, fetchguard, Phase 11, towel, tide | New |
| Services: Cache, Session, Helpers, Parser, Forms & Html, Asset Compilation, Image Resizing, Collections, Behaviors, Errors & Logging | — | — | Omit. List them in the concept map as "not provided" or "use the Go stdlib". Image resizing lives inside attachments (lagoon/attach thumbnails) |
| Console: Introduction, Setup & Maintenance, Plugin Management, Scaffolding, Utilities | Console: Introduction (tool vs app binary), Setup and maintenance, Plugin management, Scaffolding, Writing commands, Utilities | bonfire, internal/build, lagoon, cabana, surf, tide | Port |
| Console: Theme Management, Asset Compilation (all) | — | — | Omit |
| API docs tab | API reference: one page per module README | all modules | Ingested (D-08) |
Q2. Inventory and the D-08 README decision
What exists (probed this session):
- 18 modules, each with a README (2,108 lines in total, root README 151).
- READMEs follow the fixed template, and their identifier convention is backticked
`pkg.Ident`/`pkg.Type.Member`. - Zero
Example*functions exist in any module (grep '^func Example'found none). - Exported surface (
go doc -shorttop-level lines): cabana 113, tide 51, pact 44, lagoon 37, bouncer 32, surf 26, wristband 20, postcard 18, bonfire 10, fetchguard 10, towel 8, phrasebook 6, wire 5, backpack 4, boardwalk 4, compass 4, festival 4, party 3. examples/hellois a separate workspace module with three plugins (base, greeter, optional).cmd/summerholds the tool commands.internal/buildholds the scaffolder.admin/holds the Vue SPA.
Thin spots:
- Usage sections are 15–70 lines, one snippet each. There is no conceptual, cross-module narrative: how a request flows through surf, towel and a handler, or how config reaches a plugin.
- boardwalk (15 usage lines) and fetchguard (23) are the thinnest.
- The known quickstart gaps (missing
http.body_limits, the ICUpl-PLlocale requirement, a stale generatedmain.go) are documented only as root README "Known issues". - Documentation gap to log:
wristband.DefaultOptions()hard-codes a consuming-application URL. modules/wristband/server.go:100 readsResource: "https://mcp.plytarium.com/mcp",and the comment at server.go:63-64 readsconfig('fonoteka.mcp.resource'), D-03). PHP default:/"https://mcp.plytarium.com/mcp".[VERIFIED: modules/wristband/server.go:61-65, 100]. Docs pages must not quote that default (D-11). Per scope, log the gap and do not change the API in this phase. Several wristband comments (stores.go:16-17, 155, 173-174; client_issue.go:2) also name the application. They are not in READMEs, and a src= snippet that pulls them in would trip the forbidden-name test.
D-08 decision: ingest READMEs into the site as the API reference (derive, don't duplicate).
| Option | Drift | Agent usefulness | Verdict |
|---|---|---|---|
| Link out to READMEs on Gitea | none | Poor. llms-full.txt misses the API reference and the site links leave the site | Reject |
Ingest modules/*/README.md as api/<module> pages |
none (one source) | Good. The API reference appears in llms-full.txt, in search and under the identifier checker | Recommend |
| Supersede (move content to docs/, shrink READMEs) | none | Good | Reject. It violates D-08 ("READMEs remain the per-package reference") and the CLAUDE.md README structure rule |
Ingestion rules:
- title = the README H1 (the module name), description = the summary sentence (the line after the H1), section =
api, order = alphabetical. - The H1 is stripped (the template renders the title).
../x/README.mdlinks become/api/x.html(and.mdin the raw output).- Guides link to modules by the README's repo-relative path. For example,
docs/services/mail.mdlinks../../modules/postcard/README.md, which works on Gitea and is rewritten to/api/postcard.htmlon the site. - The module list is discovered dynamically. Any
modules/<name>/holding non-test.gofiles must have a README, or the content test fails. So Phase 11's new modules (jobs, realtime, search) appear without code changes (SC1: "every framework module is reachable").
Q3. llms.txt, llms-full.txt, per-page .md
llms.txt format [CITED: llmstxt.org]:
- An optional BOM.
- An H1 with the project name, the only required part.
- A blockquote summary.
- Zero or more non-heading Markdown sections.
- Zero or more H2 "file list" sections of
- [name](url)items, optionally followed by: notes. - An H2 named
Optionalfor secondary links.
Pages should offer clean Markdown "at the same URL as the original page, either with .md appended (page.html.md) or with the extension replaced by .md (page.md)". URLs without file names append index.html.md or index.md.
llms-full.txt is not in the llmstxt.org spec (the page has no mention of it) [CITED: llmstxt.org]. The de facto format, from Mintlify, repeats per page: # Title, then Source: <url>, a blank line, the description, then the full Markdown [CITED: mintlify.com/docs/llms-full.txt]. Mintlify also publishes /.well-known/llms-full.txt [CITED: mintlify.com/docs/ai/llmstxt]. Optional here.
Recommended outputs:
- URL scheme:
docs/<section>/<slug>.mdbecomes/<section>/<slug>.htmlplus/<section>/<slug>.md(the llmstxt "extension replaced" form). The landing page is/index.htmlplus/index.md. This works on every static host and overfile://, with no rewrite rules. Wording note: D-06 says "page URL plus .md". The.html-to-.mdsibling form is the spec's equivalent alternative. See Open Question 2. - Per-page
.md= transformed source, not a byte copy:- drop the frontmatter;
- start with
# Titleand> description; - rewrite internal links to sibling
.mdURLs; - reduce fence info strings to the language word (strip
src=); - keep code bodies, which already equal the sources because of the sync test.
- llms.txt:
# SummerCMS, then a>summary: the root README's first paragraph, reworded without application names.- An "Important notes" bullet list: compiled plugins, Postgres only, headless with no frontend themes, Go 1.27.
- One
## <Section>per sidebar section with- [Title](<url>.md): <description>. - The API reference as its own
## API reference. ## Optionalfor the WinterCMS concept map? No. Keep the concept map in Setup. UseOptionalfor "Frontend and AJAX (not provided)".
- llms-full.txt: every page in sidebar order, in the Mintlify block format. Links inside it stay as they are in the
.mdpages. - Base URL:
docs/site.yaml: base_url, overridable by--base-url. When empty, emit root-relative URLs. The spec's examples use absolute URLs, so a real deployment should set it (hosting is deferred).
Q4. Verified examples: mechanism choice (D-07)
Pick: include by reference with a verbatim copy and a drift test (embedmd style). It is one mechanism with three source forms:
```go src=modules/bonfire/example_test.go#ExampleNewRoot
root, err := bonfire.NewRoot("acme", []bonfire.Command{hello}, os.Stdout)
...
// Output: hello
```
| src form | Extracts | Use |
|---|---|---|
path |
whole file | small YAML, fields.yaml, columns.yaml |
path#Ident |
top-level Go declaration with its doc comment, verbatim bytes from token.FileSet offsets. For Example* idents: the body, dedented, with its // Output: comment, as godoc shows it |
API examples, walkthrough types and functions |
path#region-name |
the lines between // docs:start region-name and // docs:end region-name (# comment markers in YAML), markers excluded, dedented |
multi-declaration spans, parts of plugin.go |
Rules the checker enforces:
- Every
```gofence indocs/**/*.mdmust carrysrc=. Non-Go content uses```text,sh,yaml, and so on. YAML fences may carrysrc=, and the walkthrough's YAML must. srcpaths are repo-relative, cleaned, and must resolve inside the repo root (no..escape, no absolute paths). Go sources must sit inside the root module, so notexamples/hello/**(a separate workspace module that rootgo test ./...skips, as the root README says) and notadmin/**.- A referenced
Example*must have an// Output:comment, sogo testruns it, not just compiles it ("compiled and run", SC4). A referenced non-Example region must sit in a package directory that has_test.gofiles. - The fence body must equal the extracted text byte for byte after trailing-newline normalisation. On mismatch the test fails with a diff and the hint
run: summer docs:sync. - A missing file, ident or region fails both the test and
docs:build(D-07's "build fails if a referenced snippet is missing").
Where the example sources live:
- API examples go in
modules/<name>/example_test.go,package <name>_test, named after real identifiers (ExampleNewRoot,ExampleBus_Fire,ExampleConfig_Get, and so on).- Benefit (probed this session):
go vet, whichgo testalso runs, rejects Example names that refer to unknown identifiers. It printedExampleMissing refers to unknown identifier: Missing, vet rc=1, and failed the test build. That is a free identifier check on every example, on top of the docs checker. - These files add tests, not exported API, so no README change is needed under CLAUDE.md.
- Benefit (probed this session):
- The walkthrough goes in
docs/examples/blog/…as ordinary packages of the root module (import pathgit.golem15.com/golem15/summercms/docs/examples/blog). They are covered by rootgo vet ./...andgo test ./...automatically (root go.mod:module git.golem15.com/golem15/summercms,ignore ./admin/node_modulesonly [VERIFIED: go.mod:1,6]).- Do not put
Example<Ident>functions in a docs-only package. vet would flag them as unknown identifiers. UseExample_suffix(package-level, lowercase suffix) there. The probe acceptedExample_walkthrough.
- Do not put
- Rejected alternative, extracting fenced Markdown and compiling it (txtar or a generated
_test.go):- Snippets must then be complete units or wrapped in templates, with import boilerplate.
- gopls rename and "find references" cannot see them.
- Compiler errors point to generated files.
- It duplicates the Example machinery Go already has.
How the multi-file walkthrough stays verified. docs/examples/blog mirrors the scaffold layout that summer make:* produces:
plugin.go,routes.go;models/post.go,models/post/fields.yaml,models/post/columns.yaml;updates/…;controllers/posts.go,controllers/posts/config_form.yaml,controllers/posts/config_list.yaml;console/…,lang/en/….
The admin YAML paths come from the scaffolder: formRel := filepath.Join("controllers", snake, "config_form.yaml"), listRel := filepath.Join("controllers", snake, "config_list.yaml"), fieldsRel := filepath.Join("models", snake, "fields.yaml"), columnsRel := filepath.Join("models", snake, "columns.yaml") [VERIFIED: internal/build/artifact.go:250-254].
Its tests:
- (a)
-short-safe: activate the plugin with party and backpack, assemble routes with surf (route table assertions throughhttptest), build the console command with bonfire and run it against buffers, and compile the admin YAML through cabana if that can boot without a DB. - (b) Docker (skipped under
-short, likelagoonDB): migrate up, CRUD,RollbackLast, against testcontainers Postgres. Use a DB created withTEMPLATE template0 ... LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL', the lagoon test idiom (modules/lagoon/postgres_test.godedicatedDB). lagoon refuses any other locale. - (c) A scaffold-layout test: run
build.MakePlugin/MakeModel/MakeMigration/MakeAdminController/MakeCommandinto a temp copy ofexamples/hello, ascmd/summer'sTestMakeCommandsViaCLIdoes withcopyHelloApp. Assert that the relative file set matches the walkthrough tree, ignoringgo.mod/go.sumand generated accessor files. That pins the walkthrough to what the scaffolder really emits.
Caveat: summer make:* requires the plugin directory to be its own module with a go.mod (readModulePath(filepath.Join(pluginDir, "go.mod")) in artifact.go). The in-root walkthrough package has none. Show its go.mod as a ```text block, derived from the scaffold test's output. See Open Question 3 for the nested-module alternative.
Q5. Generator: extensions, highlighting, search, location, CLI naming
-
Extensions: see Standard Stack. Only core goldmark and stdlib.
-
Frontmatter: the file must start with
---\n. Split at the next\n---\nand decode withyaml.NewDecoder(r, yaml.DisallowUnknownField())into:struct{ Title, Description, Section string; Order int }Every field is required.
sectionmust equal the directory name, andordermust be unique within the section. The body's first line must be# <Title>(readable on Gitea, D-01). The generator strips it. -
Syntax highlighting: a stdlib
go/scannertokenizer forgofences, emitting<span class="tok-kw|tok-str|tok-com|tok-num">. Usescanner.ScanComments. It works on fragments because no parse is needed.yamlandshget a line-regex highlighter (keys, comments,$prompts), or none. Everything goes throughhtml.EscapeString. No dependency. -
Search:
search-index.jsonwith one entry per page section (H2), so hits deep-link to anchors:{"p":[{"u":"/plugins/registration.html","t":"Registration","s":"Plugins"}],"e":[{"p":0,"a":"capabilities","h":"Capabilities","x":"<plain text, ~300 chars>"}]}.- Vanilla JS lazy-fetches the index on first focus or ⌘K, does lowercase AND-token matching with a title > heading > text boost, and renders with
textContentonly. - Expected size is roughly 60 guide pages plus 21 READMEs, a few hundred KB. Fine without a prebuilt inverted index.
-
Package location: use
internal/docsite(packagedocsite). It follows the precedent of tool internals (internal/build,internal/dev) thatcmd/summercalls. Beach names are used only for public framework modules undermodules/. A module would force a README, a root-table row and a public API commitment for something host apps don't use yet (a host-app docs skill is deferred). Avoidinternal/docs, which is easy to confuse with thedocs/content directory. If the user later wants it public, a beach name such aslighthouse[ASSUMED name suggestion] can wrap it. -
Theme assets:
internal/docsite/theme/{templates/*.html, assets/site.css, assets/site.js, assets/theme-init.js, assets/fonts/*}, embedded with//go:embed. Output issite/by default, or--out. Add/site/to.gitignore: the current.gitignorehas/bin/,/dist/and the rest, but no docs output [VERIFIED: .gitignore:1-27]. -
CLI naming: bonfire accepts only the bare names
"build", "dev", "serve", "migrate", otherwisens, verb, ok := strings.Cut(name, ":")/return ok && ns != "" && verb != "" && !strings.Contains(verb, " ")[VERIFIED: modules/bonfire/command.go:110-117]. The existing tool commands follownamespace:verb:make:plugin,plugin:add,parity:proxy,parity:record,parity:replay,migrate:status[VERIFIED: cmd/summer/main.go:27-46; cmd/summer/parity.go:15,32,54].- Use
docs:build,docs:serveanddocs:sync, registered intoolCommands()in cmd/summer/main.go, not in app binaries. docs:servedoes not collide with the bareservedelegate (main.go:44 readsdelegateCommand("serve", "Run the app HTTP server")).- Flags are string flags (bonfire
Flag):--src(defaultdocs),--out(defaultsite),--base-url,--addr(default a loopback address, e.g.127.0.0.1:8088, at our discretion),--check(bare: validate without writing). - Extend
TestToolCommandNamesin cmd/summer/main_test.go with the three names.
- Use
-
Config file
docs/site.yaml:title: SummerCMS base_url: "" edit_url: "https://git.golem15.com/golem15/summercms/_edit/master/{path}" # [ASSUMED] Gitea edit URL shape source_url: "https://git.golem15.com/golem15/summercms/src/branch/master/{path}" # [ASSUMED] sections: [setup, architecture, plugins, backend, database, services, console, api] # sidebar order, with display titlesDecode it strictly. Section display titles live here so the sidebar order is data.
-
The walker excludes
docs/examples/**(Go and YAML sources, not pages),docs/site.yaml, and any_-prefixed file or directory.
Q6. Identifier checker and link/anchor checker (D-12)
Identifier checker (stdlib, prototype-validated):
-
Where it looks: inline code spans (
ast.CodeSpan) indocs/**/*.mdandmodules/*/README.md(and the rootREADME.md). Fenced blocks are skipped, since compilation already verifies them. -
Span grammar:
^\*?(<pkg>)\.(<Ident>)(\.<Member>)?(\[[^\]]*\])?(\(.*\))?$<pkg>is one of the discovered module names (the directory names undermodules/, discovered live, so Phase 11 modules are included). A leading*and trailing generic args or a call suffix are allowed. That covers README forms like`compass.Load("config")`and`surf.Assemble(app, plugins)`. Spans whose first segment is not a module name are ignored:http.Handler,fields.yaml,acme.blog,summer.yaml,context.Background(). That is the main false-positive guard. -
Index:
go/parser.ParseFileover each module directory's non-test.gofiles, recording:- top-level funcs, types, consts and vars;
- methods keyed by receiver base type (strip
*,IndexExpr,IndexListExprfor generics); - struct fields, including embedded names;
- interface methods.
-
Fallback on miss: shell out to
go doc ./modules/<pkg> <Ident>[.<Member>]before failing. That catches promoted members through embedding without re-implementing type checking. It runs only on misses, so it stays cheap. -
Sub-packages (for example
lagoon/attach): allowattach.Xspans by also indexingmodules/<m>/<sub>/dirs holding Go files, keyed by the last path element. If two sub-packages share a name, fail loudly and require the spelled-out form. -
Evidence: a throwaway prototype (about 100 lines, scratchpad, not committed) ran over the current READMEs and reported
checked=742 misses=0. -
Unexported / lowercase second segments (
backpack.app, config keys such asmail.driver) are ignored. Config-key checking is out of scope (Open Question 4).
Link and anchor checker:
- Walk
ast.Linkandast.Imagein every page (guides plus ingested READMEs). - Classify each destination:
http(s)://→ allowed, not fetched (no network in tests).#frag→ must match a heading ID in the same page.- relative
*.md(optionally#frag) → must resolve to a page in the tree or to an ingestedmodules/<m>/README.md, and#fragmust exist in the target. ../../modules/<m>/README.md→ allowed (rewritten).- Other repo paths (
.planning/**,examples/**, source files) → fail in guide pages (they break on the site). Use asource_urlshortcode or a full forge URL. mailto:→ allowed.
- Anchors come from the same
parser.IDsimplementation the renderer uses. One function, so the site and the checker cannot disagree. - Lint: no links or code spans inside headings. goldmark builds heading IDs from the raw last source line (atx_heading.go
generateAutoHeadingIDreadslastLine.Value(reader.Source())), so a link URL would leak into the ID.
CLI command-name checker:
- Parse (go/ast) every
bonfire.Command{Name: "<lit>"}composite literal and everydelegateCommand("<lit>", …)call undermodules/andcmd/summer. Current names [VERIFIED: modules/lagoon/commands.go:19,32,53; keygen.go:15; modules/cabana/commands.go:21,35; modules/surf/routelist_command.go:17; cmd/summer/main.go:27-46; parity.go:15,32,54]:migrate,migrate:rollback,migrate:status,key:generateadmin:create,admin:reset-passwordroute:list- the
make:*family,plugin:add,dev,build parity:proxy,parity:record,parity:replay- plus
serve
- Any
summer <cmd>or./bin/<app> <cmd>token inshfences or code spans must be in that set.
Forbidden-name check (D-11):
- Case-insensitive
fonoteka|płytarium|plytariumover the built outputs: every.md,llms*.txt, rendered HTML and search index. Checking the outputs covers text pulled in through src= snippets too. - Currently clean:
grep -rniEover README.md and modules/*/README.md returned nothing. - The list is a constant in the test and gate script.
Q7. Existing hygiene gates and this phase's gate
The pattern is scripts/check-phase<N>.sh:
set -euo pipefailrefuse()- mode flags (
--self-test,--layout,--imports,--readmes,--status,--go,--all) - a
--self-testthat plants each violation in a scratch copy and asserts the matching refusal (expect_refusal) run_gorunninggo vet ./...andgo test ./...
(Read from scripts/check-phase10.2.sh.)
Recommended scripts/check-phase11.1.sh modes:
--preconditions: the Phase 9, 10.1 and 11 packages exist. At minimum, the Phase 11 jobs/realtime/search module directories exist undermodules/with READMEs, andpact.HasScheduleis declared. Today it is only a comment: capabilities.go:303-307 reads// Future capability families are type-asserted when their first consumer/// packages exist:/// HasListeners/// HasSchedule[VERIFIED: modules/pact/capabilities.go:303-307].--deps:go.moddirect requirements unchanged versus the phase base (SC2 "only new dependency is goldmark", here none).--forbidden: the D-11 grep overdocs/and built output.--docs:go run ./cmd/summer docs:build --out "$tmp", plus asserting thatindex.html,llms.txt,llms-full.txtandsearch-index.jsonexist, and that no.gofile lands under output.--claude: CLAUDE.md's Documentation section contains the D-13 bullet.--self-test: plants a drifted snippet, an unknown identifier, a broken anchor, an unknown command, a forbidden name and a missing README, and asserts each is refused.--go:go vet ./...,go test ./....--all.
Keep all semantic checks in Go tests. The script only orchestrates them and adds repo-level assertions (deps, CLAUDE.md, preconditions).
CLAUDE.md edit (D-13 / DOCS-08). Today the section has four bullets, ending - Every identifier named in a README must exist in the package; check it with \go doc ./modules/ `.` [VERIFIED: CLAUDE.md:27-32]. Two changes:
- Add: "A change to a module's exported API, config keys or CLI commands also updates the affected pages under
docs/, in the same change.go test ./internal/docsite/...checks identifiers, links, snippets and command names indocs/and in every module README." - Amend the go doc bullet to name the automated checker.
It is an additive edit, in its own commit (a planning/docs-rules commit, separate from code).
Q8. Sequencing risk and plan breakdown
Risk: 11.1 depends on Phase 11, which has not run. Several other phases also change surfaces the docs describe:
- Phase 9 shows "In Progress" in ROADMAP. ADMIN-01..05 and AUTH-08 are still unchecked in REQUIREMENTS.
- Phase 10.1 is planned but not executed. It changes cabana, pact and admin with partials, widgets, assets and toolbar actions.
The numeric execution order (9 → 10.1 → 11 → 11.1) means all of them should land first. Planning now is safe if the plans do three things:
- Never hard-code Phase 11 package or identifier names. Phase 11's CONTEXT leaves names open ("ARCHITECTURE.md suggests
congafor jobs"). Plans say "the jobs, realtime and search modules as shipped by Phase 11" and read them at execution time. - Let the checkers enforce reality. A page naming a nonexistent identifier or module fails
go test, and the API reference is discovered dynamically. No stub pages that name future APIs. - Put a
--preconditionsgate at the start of the content plans that need those phases. If Phase 11 has not landed, those pages are simply not written. There are no "coming soon" stubs with identifiers, because stubs rot and would have to be special-cased in the checker.
Recommended breakdown (tracer first, lean, unit tests last). Present it at the plan-count checkpoint:
| # | Plan | Scope | Depends |
|---|---|---|---|
| 11.1-01 | Tracer: generator core to every output | internal/docsite load (frontmatter, site.yaml, walker), goldmark pipeline (GFM, custom IDs, transformers), README ingestion for all modules, minimal theme (sidebar + content), .md + llms.txt + llms-full.txt + search-index.json emission, the src= extractor and sync check, summer docs:build + docs:sync, /site/ gitignored. Content: docs/index.md, setup/installation.md with one verified snippet from a new modules/bonfire/example_test.go. Smoke test: build the real tree into t.TempDir() |
— |
| 11.1-02 | Site UX and accuracy gates | Full Winter-style theme per UI-SPEC (TOC, prev/next, edit link, View as Markdown, callouts, go/scanner highlighting, mobile nav, tri-state dark mode with a sync theme-init.js), search JS, docs:serve. Checkers: identifiers (docs + READMEs), links and anchors, CLI names, forbidden names, strict go-fence policy. scripts/check-phase11.1.sh. CLAUDE.md D-13 edit (separate commit) |
01 |
| 11.1-03 | Framework content A | Setup (Introduction, Installation, Configuration, Coming from WinterCMS), Architecture, Plugins, Console, plus example_test.go files for party, backpack, compass, festival, bonfire, phrasebook, towel |
02 |
| 11.1-04 | Framework content B (precondition: Phases 9, 10.1, 11 landed) | Database, Backend, Services (including jobs, realtime and search from Phase 11), the "Frontend and AJAX (not provided)" page, example_test.go files for lagoon, wire, surf, postcard, bouncer, fetchguard, cabana, and the Phase 11 modules |
03 |
| 11.1-05 | acme/blog porting walkthrough | docs/examples/blog plugin (models, updates, routes, admin controller + YAML, console command, lang), its -short and Docker tests, the scaffold-layout test, and setup/porting-a-plugin.md referencing all of it by src= |
02 (and 04 if it shows Phase 11 features, otherwise parallel with 03/04) |
| 11.1-06 | Unit tests last | Full coverage of internal/docsite (parser, IDs, extractor, every checker's failure path with planted fixtures, llms formats, search index, docs:serve handler), gate --self-test, assembled acceptance per SC |
all |
Lean alternative with 5 plans: merge 03 and 04 into one content plan. It is large (about 45 guide pages), and its precondition is then Phase 11 for all content. Prefer 6 if per-plan context is a concern.
Q9. Inputs for the UI-SPEC (/gsd-ui-phase)
- Winter features to mirror: listed in the Q1 include/omit table.
- Admin design tokens to reuse as plain CSS custom properties. No Tailwind: the admin compiles Tailwind v4 through npm (
@import "tailwindcss"), which the docs cannot use.- Light values [VERIFIED: admin/src/styles/main.css:83-106]:
--c-bg: #f4f6f9;--c-surface: #ffffff;--c-subtle: #f3f5f8;--c-border: #e6e9ef;--c-text: #141b2d;--c-muted: #566175;--c-primary: #22304d;--c-ring: rgba(252, 196, 40, 0.55);--c-sel: #fdf3cf;--c-side: #1d2740; - Dark overrides under
.dark[VERIFIED: main.css:108-125]:--c-bg: #111726;--c-surface: #182033;--c-subtle: #1f283d;--c-border: #29334b;--c-text: #eef1f6;--c-muted: #a9b3c6;--c-primary: #fcd34d; - Brand accent
#fcd34d, sidebar text#c3cbda, radii (control 10px, card 16px, pill 999px) and spacing (header 64px, panel 224px) come from the same@themeblock (main.css:24-81, as read viased). - Fonts:
--font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif;and--font-mono: "DM Mono", ui-monospace, monospace;[VERIFIED: main.css:25-26]. - Design rationale lives in
.planning/phases/10-admin-vue-spa/design/README.md: navy plus sunny yellow, and a sidebar that stays dark in both modes.
- Light values [VERIFIED: admin/src/styles/main.css:83-106]:
- Fonts: the admin gets them from
@fontsourcenpm packages. The built woff2 files inmodules/boardwalk/dist/assets/have hash-suffixed names such asdm-sans-latin-400-normal-CW0RaeGs.woff2, so do not reference them. Either vendor a small set (DM Sans 400/600/700 and DM Mono 400, latin and latin-ext) intointernal/docsite/theme/assets/fonts/with the OFL licence file (DM fonts are SIL OFL 1.1 [ASSUMED]), or use the system stack. The UI-SPEC decides. - Dark mode: the class strategy is the same as the admin (
.darkon<html>, CSS variables swap). The difference is a tri-state toggle (light/dark/system) persisted inlocalStorage, applied by a synchronous externaltheme-init.jsin<head>to avoid a flash of unthemed content. Use an external file rather than an inline script so the site keeps working under ascript-src 'self'CSP, consistent with Phase 10.1 D-16. - Callouts: use GitHub-style
> [!NOTE],> [!WARNING]and> [!TIP]in source, detected by an AST transformer. Gitea renders these [ASSUMED], and Winter's> **NOTE:**style also stays readable.
Architecture Patterns
System architecture diagram
docs/**/*.md ──┐ modules/*/README.md ──┐ docs/site.yaml
(frontmatter) │ (ingested as api/*) │ (sections, urls)
v v │
┌─────────────────────────── Load ───────────────┴──┐
│ walk + strict frontmatter + H1==title + order │
└───────────────┬───────────────────────────────────┘
v
┌──── Parse (goldmark: GFM, custom IDs) ────┐
│ AST transformers: link rewrite, callouts, │
│ H1 strip, fence src= resolution │
└───┬───────────────┬───────────────────┬────┘
│ │ │
v v v
┌── Verify ──────┐ ┌─ Render ─────────┐ ┌─ Emit AI/search ─────┐
│ snippet==src │ │ html/template: │ │ page.md (clean) │
│ identifiers │ │ sidebar, TOC, │ │ llms.txt │
│ links/anchors │ │ prev/next, edit, │ │ llms-full.txt │
│ CLI names │ │ go/scanner hl │ │ search-index.json │
│ forbidden names│ └────────┬──────────┘ └──────────┬───────────┘
└──────┬─────────┘ v v
│ errors → fail site/**.html + embedded theme assets ──> docs:serve (loopback)
v
go test ./... (same Verify functions over the real tree)
^
modules/*/example_test.go (// Output:) ─ run by go test, names checked by go vet
docs/examples/blog/** ─ compiled + tested in root module, referenced by src=
Recommended structure
docs/
├── site.yaml # title, base_url, edit/source URL templates, section order
├── index.md # landing
├── setup/ # introduction, installation, configuration, coming-from-wintercms, porting-a-plugin
├── architecture/ # introduction, go-modules-and-workspaces, application-lifecycle, request-lifecycle
├── plugins/ # registration, migrations, configuration-and-settings, localization, scheduling, extending, testing
├── backend/ # admin-controllers, forms, lists, relations, users-and-permissions, partials-and-widgets, admin-spa
├── database/ # connection, migrations, queries, models, relations, attachments, casts, serialization, validation
├── services/ # container, events, routing, rate-limiting, responses, authentication, oauth-server, mail, storage, hashing, outbound-http, jobs, realtime, search, parity-testing, frontend-and-ajax
├── console/ # introduction, setup-and-maintenance, plugin-management, scaffolding, writing-commands, utilities
└── examples/blog/ # NOT pages: the verified acme/blog plugin (Go + YAML + tests)
internal/docsite/
├── load.go parse.go ids.go render.go llms.go search.go snippet.go
├── check_identifiers.go check_links.go check_commands.go check_forbidden.go
├── serve.go
├── theme/ (templates/, assets/) # //go:embed
└── *_test.go + testdata/ (planted-violation fixtures)
cmd/summer/docs.go # docs:build, docs:serve, docs:sync → internal/docsite
modules/<m>/example_test.go # package <m>_test, Example<Ident> with // Output:
scripts/check-phase11.1.sh
Pattern: one verify function, two callers
docsite.Check(root) []Problem is called by docs:build (which refuses to write when problems exist) and by TestDocsTree in internal/docsite, which runs on ../../docs and ../../modules. Problems carry file:line: rule: message so agents can fix them mechanically.
Anti-patterns to avoid
- Hand-written
```gowithoutsrc=. It is unverified by construction. The checker rejects it. - Examples in
examples/hello. Rootgo test ./...skips that workspace module, so it does not satisfy SC4. Example<RealIdent>in a docs-only package. vet fails with "refers to unknown identifier".- Generating the site into
docs/. The walker would then read its own output. Usesite/and gitignore it. - A second heading-ID algorithm in the checker. Share the renderer's
parser.IDs. - Rendering search results with
innerHTML. UsetextContent, as with Phase 10 T-10-16 hygiene.
Don't Hand-Roll
| Problem | Don't build | Use instead | Why |
|---|---|---|---|
| Markdown parsing, GFM tables | a regex Markdown renderer | goldmark (in tree) | CommonMark edge cases |
| YAML frontmatter and config | a hand parser | goccy/go-yaml with DisallowUnknownField() |
Typos in frontmatter must fail |
| Go tokenizing for highlighting | regex over Go | go/scanner |
Raw strings, runes, comments and nesting are handled exactly |
| Example discovery and "is it runnable" | a custom test runner | Go Example functions with // Output: (and go/doc.Examples if you need Example metadata) |
go test runs them, and vet validates names |
| Promoted-member resolution in the identifier checker | a type checker | go doc shell-out only on AST miss |
Authoritative, rare path |
| HTML escaping in chrome | string concatenation | html/template |
Contextual escaping |
Common Pitfalls
Pitfall 1: Snippet drift invisible on the git host
What goes wrong: the Markdown shows old code that still "looks right". Why: copies are static. Avoid: a byte-exact sync test plus docs:sync, and a build that refuses. Warning sign: a PR touching modules/*/example_test.go without touching docs/. The test catches it.
Pitfall 2: SC4 silently unmet by -short
What goes wrong: walkthrough DB tests skip under -short, so "run" becomes "compiled". Avoid: every docs-referenced Example has // Output: and needs no DB. DB-dependent walkthrough steps are Docker tests. Run the gate's --go without -short (Docker is available here: client 29.7.2).
Pitfall 3: Heading anchors differ between goldmark, Gitea and GitHub
What goes wrong: the default goldmark generator drops every multibyte character and maps _ to -. Code: if l != 1 { continue } … } else if util.IsSpace(v) || v == '-' || v == '_' { result = append(result, '-') }, and duplicates get -1, -2 [VERIFIED: goldmark parser/parser.go:99-138]. GitHub keeps _ and Unicode letters [ASSUMED]. Avoid: implement parser.IDs with a GitHub-compatible slug (lowercase, keep letters/digits/_/-, spaces to -, drop other punctuation, -N dedupe), passed through parser.WithIDs. Keep headings ASCII, without links or code, and use the same IDs in the checker.
Pitfall 4: Consuming-application names leaking through source snippets
What goes wrong: a src= region in wristband carries comments naming the application (server.go:63-64, stores.go:16-17). Avoid: run the forbidden-name check on built output, not just source Markdown. Choose regions that exclude those comments.
Pitfall 5: file:// breaks search
What goes wrong: browsers block fetch() of a JSON file from a file:// page [ASSUMED standard browser behaviour]. Avoid: document summer docs:serve for local preview. The search box shows a "search needs a web server" hint when the fetch fails. Everything else works over file:// because of the .html URLs.
Pitfall 6: Checker false positives on non-API spans
What goes wrong: spans like `fields.yaml`, `acme.blog`, `summer.yaml` look like pkg.Ident. Avoid: match only when the first segment is a discovered module name and the second starts uppercase. That rule gave 0 misses on 742 real spans.
Pitfall 7: New Phase 11 modules missing READMEs or API pages
What goes wrong: the sidebar misses a module, which violates SC1. Avoid: the dynamic module discovery test fails when any modules/<m>/ with non-test .go files lacks a README.
Pitfall 8: go.mod churn
What goes wrong: someone adds chroma or x/tools "just for the checker". Avoid: the gate's --deps compares direct requirements with the phase base commit.
Code Examples
Custom heading IDs wired into goldmark
// Source: goldmark parser.WithIDs (parser/parser.go:240-244), IDs interface (81-87)
md := goldmark.New(
goldmark.WithExtensions(extension.GFM),
goldmark.WithParserOptions(parser.WithAutoHeadingID()),
)
ctx := parser.NewContext(parser.WithIDs(newSlugIDs())) // same type the link checker uses
doc := md.Parser().Parse(text.NewReader(src), parser.WithContext(ctx))
Fence reference resolution
// Source: goldmark ast.FencedCodeBlock.Language / Info (ast/block.go:302-321)
if fc, ok := n.(*ast.FencedCodeBlock); ok {
lang := string(fc.Language(src))
info := string(fc.Info.Segment.Value(src)) // e.g. `go src=modules/bonfire/example_test.go#ExampleNewRoot`
ref, hasRef := parseSrc(info) // strings.Fields + "src=" prefix
_ = lang; _ = ref; _ = hasRef
}
Example that vet and test both verify
// modules/festival/example_test.go — illustrative; vet rejects the name if Bus.Fire disappears.
package festival_test
func ExampleBus_Fire() {
// ... construct a festival.Bus, Listen, Fire, print deterministic output ...
// Output: ...
}
(festival.Bus.Fire exists: func (b *Bus) Fire[T any](ctx context.Context, event T) error, per go doc -all ./modules/festival this session. Note that it is a generic method, a Go 1.27 feature, so the Example naming ExampleBus_Fire follows the method rule.)
State of the Art
| Old approach | Current approach | Impact |
|---|---|---|
| HTML-only docs | llms.txt index + per-page .md + llms-full.txt |
Agents read without scraping. The format is settled enough (llmstxt.org spec; Mintlify's llms-full convention) |
> **NOTE:** callouts |
> [!NOTE] alerts (GitHub, and Gitea [ASSUMED]) |
Rendered natively on forges and by our transformer |
Manual go doc spot checks for README identifiers |
An automated AST checker in go test |
Replaces the CLAUDE.md manual rule's mechanics |
Assumptions Log
| # | Claim | Section | Risk if wrong |
|---|---|---|---|
| A1 | The Gitea edit URL is /<owner>/<repo>/_edit/<branch>/<path> and the view URL is /src/branch/<branch>/<path> |
Q5 site.yaml | Edit links 404. Config-only fix |
| A2 | Gitea renders > [!NOTE] alerts and ```go src=... fences with Go highlighting (language = first word) |
Q9, Q4 | The source reads slightly worse on Gitea. Site unaffected |
| A3 | GitHub/Gitea slug rules keep _ and Unicode letters |
Pitfall 3 | Anchors from the git host differ on non-ASCII headings. Mitigated by the ASCII-heading lint |
| A4 | DM Sans and DM Mono are SIL OFL 1.1 and can be vendored with the licence file | Q9 | Use the system font stack instead |
| A5 | fetch() of local JSON fails over file:// in major browsers |
Pitfall 5 | None if wrong (search then works over file:// too) |
| A6 | cabana can compile a plugin's admin YAML at boot without a database connection | Q4 walkthrough (a) | That assertion moves to the Docker test tier |
| A7 | lighthouse as a beach name if the generator is ever made public |
Q5 | Naming only |
| A8 | The master branch name for edit links is master |
Q5 | Matches the current git branch (master). Config-only |
Open Questions (for the plan-count checkpoint)
- Syntax highlighting: is stdlib Go-only highlighting (plus a trivial YAML/sh highlighter) enough, or approve
alecthomas/chroma/v2?- Recommendation: stdlib, no new dependency.
- URL form for raw pages: D-06 says "page URL plus .md". The recommendation is
/x/page.htmlwith/x/page.md(the llmstxt "extension replaced" form, portable to any host and tofile://). The literal alternative,/x/page.html.md, is also spec-compliant but uglier.- Recommendation:
.html/.mdsiblings. Confirm.
- Recommendation:
- Walkthrough as an in-root package vs a nested plugin module: in the root module,
go test ./...covers it for free. A nested module (owngo.mod, ingo.work, likeexamples/hello/plugins/*) is truer to real plugins, but rootgo test ./...skips it, so a root test would have to shell out togo testin that dir.- Recommendation: in-root package plus the scaffold-layout test.
- Config-key checking (D-13 mentions config keys): out of scope for the checker in this phase? Checking
mail.driver-style spans against embedded default YAML is feasible later.- Recommendation: defer and note it in CLAUDE.md.
- The wristband default names the consuming application's URL (server.go:100). Log it as a follow-up todo (a framework default should be empty or neutral). Do not change the API in this phase, per the phase boundary.
- Final package names for the Phase 11 modules are unknown until Phase 11 executes. The content plan for Services reads them at execution time.
- Plan count: 6 plans (recommended) or 5 (merged content).
Environment Availability
| Dependency | Required by | Available | Version | Fallback |
|---|---|---|---|---|
| Go toolchain | everything | ✓ | go1.27.0 linux/amd64 | — |
| git | edit links, gate --deps diff |
✓ | 2.55.0 | — |
| Docker | walkthrough DB tests, lagoon tests | ✓ | client 29.7.2 | -short skips DB tests (SC4 then partially unverified. The gate runs without -short) |
| Node.js | not required | — | — | — (the docs must not need it) |
| Network | none at test time | — | — | Link checker never fetches external URLs |
go vet ./... is currently green (rc 0, about 0.9 s). Nothing blocks execution.
Validation Architecture
Test framework
| Property | Value |
|---|---|
| Framework | Go stdlib testing (the repo uses no testify in tests: grep found 0 _test.go importing it) |
| Config file | none. Tests locate docs/ and modules/ through ../../ from internal/docsite |
| Quick run command | go test -short ./internal/docsite/... ./cmd/summer/... ./docs/examples/... |
| Full suite command | go vet ./... && go test ./... (Docker required for the walkthrough and lagoon DB tests) |
| Phase gate | scripts/check-phase11.1.sh --all |
Phase requirements → test map
| Req | Behaviour | Type | Command | Exists? |
|---|---|---|---|---|
| DOCS-01 / SC1 | Strict frontmatter, section == dir, unique order, H1 == title, allowed sections, every module has a README and a sidebar entry | unit (real tree) | `go test ./internal/docsite -run 'TestContentTree | TestEveryModuleInSidebar'` |
| DOCS-02 / SC2 | Build writes html for every page with sidebar, TOC, prev/next, edit link, search input and theme toggle markers. Assets embedded. No node/npm invoked |
unit + smoke | go test ./internal/docsite -run TestBuildSite; go test ./cmd/summer -run TestToolCommandNames |
❌ / extend existing |
| DOCS-02 / SC2 | go.mod direct requires unchanged |
gate | scripts/check-phase11.1.sh --deps |
❌ |
| DOCS-02 / SC2 | Search works and dark mode toggles visually | manual (no JS runner without Node) | UAT in /gsd-verify-work via summer docs:serve |
manual |
| DOCS-03 / SC3 | The sets of nav pages, *.html, *.md, llms.txt links and llms-full Source: lines are equal and in sidebar order. llms.txt matches the spec shape (H1, blockquote, H2 lists) |
unit | go test ./internal/docsite -run TestAIOutputsInSync |
❌ |
| DOCS-04 / SC4 | Every go fence has src=. Bodies equal the sources. Referenced Examples have Output. Missing ref → error | unit (real tree + planted fixtures) | go test ./internal/docsite -run 'TestSnippets' |
❌ |
| DOCS-04 / SC4 | Examples compile and run, names valid | existing toolchain | go test ./modules/... (Examples run), go vet ./... |
❌ (no examples yet) |
| DOCS-05 / SC4 | Identifier, link/anchor, CLI-name and forbidden-name checkers fail on planted violations and pass on the real tree (docs + READMEs) | unit | `go test ./internal/docsite -run 'TestIdentifiers | TestLinks |
| DOCS-06 / SC5 | The concept-map page exists and its identifiers pass | unit | covered by TestContentTree (required page list) + TestIdentifiers |
❌ |
| DOCS-07 / SC5 | Walkthrough plugin compiles, registers and serves routes, the command runs, migrations go up and down, and the scaffold layout matches | unit + Docker integration | go test ./docs/examples/... (full) / -short |
❌ |
| DOCS-08 | CLAUDE.md contains the D-13 rule | gate | scripts/check-phase11.1.sh --claude |
❌ |
Sampling rate
- Per task commit:
go vet ./... && go test -short ./... - Per plan merge:
go test ./...(with Docker) plusscripts/check-phase11.1.sh --docs --forbidden - Phase gate:
scripts/check-phase11.1.sh --allgreen before/gsd-verify-work, then manual UAT of search and dark mode indocs:serve
Wave 0 gaps
internal/docsite/package with test scaffolding and atestdata/planted-violation corpusmodules/bonfire/example_test.go(first verified snippet, tracer)docs/site.yaml,docs/index.md,docs/setup/installation.mdcmd/summer/docs.go+TestToolCommandNamesextensionscripts/check-phase11.1.shwith--self-test/site/in.gitignore
Security Domain
security_enforcement is absent from config, so it counts as enabled. The surface is small: a local build tool, a loopback preview server and static output.
| ASVS category | Applies | Control |
|---|---|---|
| V2 Authentication / V3 Session / V4 Access control | no | Static site, no auth |
| V5 Input validation | yes | Strict YAML (DisallowUnknownField). src= paths cleaned and confined to the repo root (reject .., absolute paths, symlink escapes via filepath.EvalSymlinks). Raw HTML disabled in goldmark |
| V12 Files and resources | yes | Output confined to --out (refuse --out equal to or inside --src, or the repo root). No writes outside it |
| V14 Configuration | yes | docs:serve binds loopback by default. Refuse non-loopback unless explicitly passed |
| V6 Cryptography | no | — |
| Threat | STRIDE | Mitigation |
|---|---|---|
T-11.1-01 Path traversal through src= pulls secrets (for example .env) into public docs |
Information disclosure | Confine to the repo, root module only, deny dotfiles and .env*, and run the forbidden-content test on output |
| T-11.1-02 XSS through Markdown raw HTML or search results | Tampering | goldmark safe mode (no WithUnsafe), html/template chrome, search rendering with textContent |
T-11.1-03 docs:serve exposed on the LAN |
Information disclosure | Default 127.0.0.1. Warn and require an explicit flag for other hosts |
| T-11.1-04 Consuming-application details published in framework docs | Information disclosure (policy) | The D-11 forbidden-name test over built output |
T-11.1-05 docs:build --out deletes the wrong directory when cleaning |
Tampering | Only remove the contents of an output directory that contains the generator's marker file (for example .summer-docs). Otherwise refuse |
Sources
Primary (HIGH: read or probed this session)
go.mod:1-30,.gitignore:1-27,CLAUDE.md:27-32,cmd/summer/main.go:27-46,cmd/summer/parity.go:12-57,modules/bonfire/command.go:108-119,modules/pact/capabilities.go:300-311,modules/postcard/templates.go:1-30,modules/wristband/server.go:56-105,modules/lagoon/commands.go:14-55,modules/lagoon/keygen.go:11-17,modules/cabana/commands.go:16-37,modules/surf/routelist_command.go:13-20,internal/build/artifact.go:240-255,admin/src/styles/main.css:21-26, 78-125,admin/src/app/theme.ts:1-6(Read tool)- goldmark v1.8.6 source in the module cache:
parser/parser.go:80-139, 238-245,parser/atx_heading.go:44-67,extension/gfm.go,ast/block.go:302-321 - proxy.golang.org
@latestand.modfor goldmark, goldmark-highlighting/v2, chroma/v2, goldmark-meta, goldmark/toc, x/tools - Local probes:
go vetExample-name check (scratch module), identifier-checker prototype (742 spans, 0 misses),go docbehaviour on members and misses, Docker/Go/git versions
Secondary (CITED: official pages fetched; the seam rates webfetch LOW)
- https://wintercms.com/docs/v1.2/docs/setup/installation and /database/model (sidebar tree, anatomy)
- https://raw.githubusercontent.com/wintercms/docs/develop/plugin/registration.md (source conventions, tone)
- https://llmstxt.org/ (llms.txt format,
.mdURL rule) - https://www.mintlify.com/docs/llms-full.txt, https://www.mintlify.com/docs/ai/llmstxt (llms-full convention)
Tertiary (LOW)
- Gitea URL shapes and alert rendering (A1, A2), font licences (A4)
Metadata
Confidence breakdown:
- Standard stack: HIGH. No new dependencies, and every API used was read in the module cache.
- Architecture: HIGH for the mechanisms (probed). MEDIUM for the page list, which is a judgment call inside Claude's discretion.
- Pitfalls: HIGH for 1, 2, 3, 4, 6, 7, 8 (grounded in code or probes). MEDIUM for 5.
Research date: 2026-09-28 Valid until: about 2026-10-28 for the stack. The content inventory must be re-read when 11.1 executes, after Phases 9, 10.1 and 11 land.