Files
summercms/.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-01-PLAN.md
Jakub Zych 6f57604028 docs(11.1): create phase plan
Six plans (tracer generator, site UX and checkers, content A, content B,
acme/blog walkthrough, unit tests). SC4/DOCS-04 narrowed to docs/ pages per
D-18; README Go fence conversion logged as a todo.
2026-09-30 20:33:51 +02:00

35 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, assumption_delta_decision, user_setup, estimate, must_haves
phase plan type wave depends_on files_modified autonomous requirements assumption_delta_decision user_setup estimate must_haves
11.1-summercms-documentation-for-humans-and-ai-agents 01 execute 1
internal/docsite/docsite.go
internal/docsite/load.go
internal/docsite/render.go
internal/docsite/emit.go
internal/docsite/snippet.go
internal/docsite/docsite_test.go
internal/docsite/theme/templates/page.html
internal/docsite/theme/assets/site.css
cmd/summer/docs.go
cmd/summer/docs_test.go
cmd/summer/main.go
cmd/summer/main_test.go
modules/bonfire/example_test.go
docs/site.yaml
docs/index.md
docs/setup/installation.md
.gitignore
README.md
true
DOCS-01
DOCS-02
DOCS-03
DOCS-04
no-change
tokens raw_tokens tasks confidence
95000 95000 3 low
truths prohibitions artifacts key_links
Per D-01, every page under docs/ is Markdown that starts with YAML frontmatter (title, description, section, order) followed by a first body line `# <title>`, so the file reads on the git host without a build.
Per D-02 and D-03, `summer docs:build` renders docs/ through stdlib html/template and github.com/yuin/goldmark into a self-contained static directory (default site/, gitignored) and never invokes Node or npm.
Per D-05, the build writes llms.txt (H1 `# SummerCMS`, a blockquote summary, H2 link lists with `- [Title](url.md): description` items) and llms-full.txt (every page in reading order as `# Title`, `Source: <url>`, blank line, description, full Markdown) at the output root.
Per D-06 and D-17, every page is written as <section>/<slug>.html with a clean Markdown sibling <section>/<slug>.md (index.html and index.md for the landing page), and the .md has no frontmatter, starts with `# Title` and `> description`, links to sibling .md URLs and reduces fence info strings to the language word.
Per D-07, a fence whose info string carries `src=<path>[#<fragment>]` must equal the extracted source byte for byte (after trailing-newline normalisation); a drifted or missing snippet is a problem that fails go test and makes docs:build write nothing, and `summer docs:sync` rewrites drifted copies.
Per D-08, every directory under modules/ that holds non-test Go files is published as api/<name>.html from its README.md (title = README H1, description = the summary line, section api, alphabetical order, H1 stripped, ../x/README.md links rewritten to the api page); such a directory without README.md is a `readme:` problem.
A frontmatter problem (unknown field, missing field, section not matching the directory, duplicate order within a section, first body line other than `# <title>`, description over 160 characters) is reported as `<file>:1: frontmatter: <detail>` using the UI-SPEC wording.
docs:build refuses to clean a non-empty --out directory that has no .summer-docs marker, and refuses an --out equal to the repository root, inside --src, or containing --src, printing the UI-SPEC output-guard copy.
Heading IDs come from one slug implementation passed to goldmark through parser.WithIDs: lowercase, letters, digits, `_` and `-` kept, spaces to `-`, other punctuation dropped, duplicates suffixed -1, -2.
cmd/summer tests TestDocsTree, TestDocsBuildRealTree and TestDocsAIOutputsInSync pass on the real repository tree, and TestToolCommandNames lists docs:build and docs:sync.
An empty section listed in docs/site.yaml is a `section:` problem, never an empty successful build.
A src= path that is absolute, escapes the root after cleaning or through a symlink, names a dotfile or a `.env` file, sits in a directory with its own go.mod (examples/hello, admin), names an Example without a `// Output:` comment, or names a region in a _test.go function that no Test or Example function runs is a snippet problem.
statement verification
llms.txt and the per-page .md files are useful to an AI agent reading raw files (spec shape, predictable URLs). backstop
requirement_id category status verification resolution reason statement
DOCS-02 privacy resolved judgment Theme assets are embedded and emitted under /assets/; templates reference only base_url-prefixed paths. Framework docs are served to developers; a third-party request leaks visitor data and breaks offline preview. The built site must not reference any third-party origin (web fonts, scripts, analytics, CDNs); every asset URL is under the site's own /assets/ path.
path provides contains
internal/docsite/docsite.go Options, Problem, Result, SyncResult, Build, Check, Sync entry points func Build(
path provides contains
internal/docsite/load.go strict site.yaml and frontmatter decoding, page walk, README ingestion DisallowUnknownField
path provides contains
internal/docsite/render.go goldmark pipeline with GFM, shared slug IDs and AST transformers parser.WithIDs
path provides contains
internal/docsite/emit.go html, .md, llms.txt, llms-full.txt and search-index.json emission llms-full.txt
path provides contains
internal/docsite/snippet.go src= parsing, confinement, extraction and drift detection docs:start
path provides contains
cmd/summer/docs.go summer docs:build and docs:sync commands docs:build
path provides contains
modules/bonfire/example_test.go first verified Example referenced by docs // Output:
path provides contains
docs/setup/installation.md first guide page with a src= snippet src=modules/bonfire/example_test.go#
from to via pattern
cmd/summer/docs.go internal/docsite/docsite.go docs:build calls docsite.Build, docs:sync calls docsite.Sync docsite.(Build|Sync)
from to via pattern
internal/docsite/render.go github.com/yuin/goldmark parse context built with parser.WithIDs(slug IDs) parser.WithIDs
from to via pattern
docs/setup/installation.md modules/bonfire/example_test.go go fence info string src= reference kept in sync by the snippet check src=modules/bonfire/example_test.go#Example
from to via pattern
internal/docsite/load.go modules/*/README.md dynamic module discovery ingests each README as an api page README.md
Tracer for the whole phase: the docs generator core reaches every output. `summer docs:build` loads `docs/` (strict frontmatter, `docs/site.yaml`), ingests every module README as an API reference page, renders HTML through goldmark with shared heading IDs, and writes the `.html` pages, their `.md` siblings, `llms.txt`, `llms-full.txt` and `search-index.json`. A Go fence in `docs/setup/installation.md` is a verified copy of `modules/bonfire/example_test.go`, kept honest by a drift check and `summer docs:sync`.

Purpose: prove the architecture end to end (D-01, D-02, D-03, D-05, D-06, D-07, D-08, D-17) before plan 11.1-02 adds the Winter-style theme and the accuracy checkers, and before the content plans write pages. Per D-14 this is plan 1 of 6.

Output: internal/docsite package, summer docs:build and summer docs:sync, docs/site.yaml, docs/index.md, docs/setup/installation.md, modules/bonfire/example_test.go, real-tree smoke tests in cmd/summer.

<execution_context> @/.claude/gsd-core/workflows/execute-plan.md @/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.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-PATTERNS.md @.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-UI-SPEC.md @CLAUDE.md @cmd/summer/main.go @cmd/summer/main_test.go @modules/tide/manifest.go @internal/build/scaffold.go @modules/bonfire/call_test.go Package `internal/docsite` (import path `git.golem15.com/golem15/summercms/internal/docsite`). Later plans extend these names; keep them stable.
  • type Options struct with fields Root string (repository root; src= paths and modules/ resolve against it), Src string (docs source dir, default <Root>/docs), Out string (output dir, default <Root>/site), BaseURL string (overrides site.yaml base_url when non-empty). Plan 11.1-02 adds a Commands field.
  • type Problem struct { File string; Line int; Rule string; Message string } with func (p Problem) String() string returning file:line: rule: message (File repo-relative, forward slashes).
  • type Result struct { Pages int; Out string }, type SyncResult struct { Snippets, Files int }.
  • func Check(opts Options) ([]Problem, error): load, verify and render in memory; writes nothing.
  • func Build(opts Options) (Result, []Problem, error): runs Check; when problems exist it writes nothing and returns them; otherwise guards and cleans Out, then writes every file.
  • func Sync(opts Options) (SyncResult, []Problem, error): rewrites drifted src= fence bodies under Src.
  • type Site struct decoded strictly from docs/site.yaml: Title, Description, BaseURL (base_url), EditURL (edit_url, with a {path} token), SourceURL (source_url, with a {path} token), LLMSNotes []string (llms_notes), Sections []Section (sections, each {name, title} in sidebar order).
  • type Frontmatter struct { Title, Description, Section string; Order int } (yaml keys title, description, section, order, all required).
  • type Page struct with Source (repo-relative source path), URL (extension-less path such as setup/installation, api/lagoon, index), Section, Title, Description, Order, Module (non-empty for api pages), and the Markdown body.
  • type Ref struct { Path, Fragment string }, func ParseSrc(info string) (Ref, bool), func Extract(root string, ref Ref) (string, error).
  • const MarkerFile = ".summer-docs".

Reading order (used by pager, llms-full.txt and the AI sync test): index first, then each site.yaml section in order with its pages sorted by order, with the api section's pages sorted by module name.

Task 1: Tracer: `summer docs:build` turns docs/index.md and docs/setup/installation.md into a site with every output internal/docsite/docsite.go, internal/docsite/load.go, internal/docsite/render.go, internal/docsite/emit.go, internal/docsite/theme/templates/page.html, internal/docsite/theme/assets/site.css, cmd/summer/docs.go, cmd/summer/main.go, cmd/summer/main_test.go, cmd/summer/docs_test.go, docs/site.yaml, docs/index.md, docs/setup/installation.md, .gitignore - cmd/summer/main.go (toolCommands slice, command shape, flagTrue helper) - cmd/summer/main_test.go (TestToolCommandNames want list and helpWants) - modules/tide/manifest.go lines 85-104 (strict goccy/go-yaml decode idiom) - internal/build/scaffold.go lines 20-23 (//go:embed plus template.ParseFS idiom) - modules/postcard/templates.go lines 1-30 (existing goldmark use; do not change it) - .planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-RESEARCH.md sections Q3, Q5 and "Security Domain" - .planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-UI-SPEC.md sections "Copywriting Contract" (CLI output table) and "Theme Parts" - .gitignore (the "# Go" block) Build the thinnest production-quality path through every layer the generator touches, per D-01, D-02, D-03, D-05, D-06 and D-17.
  1. Create internal/docsite with the API in the plan's interfaces block. load.go: decode docs/site.yaml with goccy/go-yaml yaml.NewDecoder(bytes.NewReader(raw), yaml.DisallowUnknownField()) into Site (the tide ParseManifest idiom, errors prefixed docsite:). Walk Src for *.md, skipping examples/, site.yaml and any _-prefixed file or directory. Each page file must start with a --- line; split frontmatter at the next --- line and decode it strictly into Frontmatter. Report UI-SPEC problem lines <file>:1: frontmatter: <detail> for: missing field, unknown field, section not equal to the directory name, duplicate order in a section (order N already used by <other>), first body line not # <title>, description over 160 characters. docs/index.md is the only root-level page; its section is the reserved value index, it is not listed in site.yaml, and it is rendered first in reading order. Every site.yaml section must have at least one page, else report docs/site.yaml:<line>: section: "<name>" has no pages (add the section in the same change as its first page).
  2. render.go: one goldmark.New with extension.GFM and parser.WithAutoHeadingID(). Do not enable goldmark's raw-HTML (unsafe) renderer option: raw HTML in Markdown stays escaped (T-11.1-02). Strip the page's # Title H1 with an AST transformer so the template renders the title once.
  3. emit.go: render everything into an in-memory map of output path to bytes before touching disk. For each page write <URL>.html from theme/templates/page.html (html/template, embedded with //go:embed) and <URL>.md (no frontmatter; # Title, blank line, > description, blank line, body). Write llms.txt (H1 # SummerCMS, > site description, the llms_notes as a bullet list, a ## Overview list holding the index page, then one ## <Section title> list per section with - [Title](<base>/<URL>.md): <description>), llms-full.txt (per page in reading order: # Title, Source: <base>/<URL>.html, blank line, description, blank line, the page .md body) and search-index.json shaped {"p":[{"u":"<base>/<URL>.html","t":"<title>","s":"<section title>"}],"e":[]} (the e heading entries arrive in Task 2). Copy theme/assets/site.css to assets/site.css. URLs are base_url plus / plus the path; with an empty base they are root-relative.
  4. page.html: <html lang="en">, <title> "{Page title} · SummerCMS docs" (index: "SummerCMS documentation"), <meta name="description">, <link rel="stylesheet" href="{base}/assets/site.css">, <body class="docs">, a <nav class="sidebar" aria-label="Documentation"> grouped by section in site.yaml order with the current item marked aria-current="page", and <main id="content"> with the H1 title, the description lead and the rendered body. Header, on-page TOC, pager, search, theme toggle and highlighting are plan 11.1-02 (UI-SPEC theme); this template is the shell that plan replaces. site.css holds a readable two-column layout with the UI-SPEC light tokens only.
  5. Output guard (T-11.1-03): Build refuses Out equal to Root, inside Src, or an ancestor of Src or Root (docs:build: --out must not be inside --src or equal to the repository root). An existing non-empty Out without .summer-docs is refused with docs:build: refusing to clean {out}: it has no .summer-docs marker. Remove the directory or choose another --out.; with the marker, remove its contents, write the files and re-create the marker. Never delete anything outside Out.
  6. cmd/summer/docs.go: docsBuildCommand() named docs:build, description "Build the documentation site", string flags root (default .), src, out, base-url, and bare flag check (validate only, write nothing). It resolves defaults, calls docsite.Build (or docsite.Check with --check), prints each problem with out.Printf("%s\n", p), then docs:build: {n} problems, nothing written and returns a short error so the binary exits 1; on success prints docs:build: wrote {n} pages to {out}. Register it in toolCommands(); add docs:build to the TestToolCommandNames want list and --out to its helpWants.
  7. Content: docs/site.yaml with title: SummerCMS, description "SummerCMS is a content management framework for Go, inspired by WinterCMS.", base_url: "", edit_url: "https://git.golem15.com/golem15/summercms/_edit/master/{path}", source_url: "https://git.golem15.com/golem15/summercms/src/branch/master/{path}", llms_notes (compiled plugins registered at build time; Postgres only; headless, no frontend themes; Go 1.27), and sections listing only setup (title "Setup") and api (title "API reference"). docs/index.md: frontmatter title "SummerCMS documentation", the UI-SPEC index description, section index, order 0; a short landing body linking setup/installation.md. docs/setup/installation.md: title "Installation", section setup, order 20; requirements (Go 1.27, PostgreSQL 16 with the ICU locale lagoon checks, Docker only for integration tests) and installing the tool with go install ./cmd/summer, as prose and sh fences. Use neutral names only (D-11).
  8. .gitignore: add /site/ in the "# Go" block next to /dist/.
  9. cmd/summer/docs_test.go: TestDocsTree calls docsite.Check with Root: "../.." and fails listing every problem line; TestDocsBuildRealTree runs docs:build --root ../.. --out <t.TempDir()>/site through bonfire.NewRoot("summer", toolCommands(), &buf) and asserts index.html, index.md, setup/installation.html, setup/installation.md, llms.txt, llms-full.txt, search-index.json, assets/site.css and .summer-docs exist and the output line matches docs:build: wrote N pages to.

Commit rule for every task in this plan: another plan (11-08) may have uncommitted edits in the working tree; stage only the files this task lists (never git add -A). Do not modify modules/lagoon/transaction.go, modules/lagoon/transaction_test.go, modules/lagoon/README.md, modules/cabana/crud.go, modules/cabana/relation.go or modules/beachcomber/sync_test.go. go vet ./internal/docsite/... ./cmd/summer && go test ./internal/docsite ./cmd/summer -run '^(TestDocsTree|TestDocsBuildRealTree|TestToolCommandNames)$' -count=1 -v <fails_when>non-zero exit, a "--- FAIL" line, or "no tests to run" in the output</fails_when> <acceptance_criteria> - go test ./cmd/summer -run '^TestDocsBuildRealTree$' -count=1 -v prints --- PASS: TestDocsBuildRealTree. - go run ./cmd/summer docs:build --out "$(mktemp -d)/site" exits 0 and prints a line starting docs:build: wrote . - grep -n 'DisallowUnknownField' internal/docsite/load.go finds a match. - grep -n '"docs:build"' cmd/summer/main_test.go finds a match. - grep -n '^/site/$' .gitignore finds a match. - ! grep -rn 'WithUnsafe' internal/docsite (no match). - head -1 docs/setup/installation.md prints --- and the first line after the closing frontmatter delimiter is # Installation. - Pointing --out at a scratch dir that holds an unrelated file makes docs:build exit non-zero and print has no .summer-docs marker, and the unrelated file still exists afterwards. </acceptance_criteria> summer docs:build writes HTML, .md siblings, llms.txt, llms-full.txt, search-index.json and assets for the index and installation pages from a strict source tree, refuses unsafe output dirs, and the real-tree smoke tests pass.

Task 2: Every framework module appears as an API reference page, with shared heading IDs, rewritten links and heading-level search entries internal/docsite/load.go, internal/docsite/render.go, internal/docsite/emit.go, internal/docsite/docsite_test.go, cmd/summer/docs_test.go - internal/docsite/load.go, internal/docsite/render.go, internal/docsite/emit.go (as written in Task 1) - modules/bonfire/README.md and modules/lighthouse/README.md (H1, summary line, `../x/README.md` link shape) - .planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-RESEARCH.md sections Q2 "Ingestion rules", Q5 "Search", Pitfall 3, Pitfall 7 - goldmark v1.8.6 in the module cache: parser/parser.go lines 80-139 and 238-245 (IDs interface, WithIDs), ast/block.go lines 302-321 Per D-08, ingest instead of duplicating, and make anchors deterministic.
  1. Module discovery (load.go): every directory modules/<name> (top level only) that contains a non-_test.go Go file is a module. Missing modules/<name>/README.md is the problem modules/<name>: readme: package has Go files but no README.md. The list is discovered at run time, never hard-coded, so Phase 11 modules (conga, lighthouse, flare, beachcomber) and any later module appear without code changes. Each README becomes a page with URL api/<name>, section api, title = the README H1 text, description = the first non-empty line after the H1, order = alphabetical by name; strip the H1 with the Task 1 transformer. The Source of these pages is the README path so problems cite it.
  2. Slug IDs (render.go): implement goldmark's parser.IDs (Generate(value []byte, kind ast.NodeKind) []byte, Put(value []byte)) as a GitHub-compatible slugger: lowercase, keep Unicode letters, digits, _ and -, map spaces to -, drop other punctuation, dedupe with -1, -2. Create a fresh instance per page and pass it with parser.NewContext(parser.WithIDs(ids)). Expose an unexported helper that returns the ID list for a page so emission, the search index and the plan 11.1-02 link checker share one algorithm.
  3. Link rewriting (AST transformer in render.go): in guide pages, a relative link to another page's .md becomes <base>/<URL>.html in HTML and <base>/<URL>.md in the raw .md; a relative link to modules/<m>/README.md (for example ../../modules/postcard/README.md) becomes the api/<m> page; in ingested READMEs ../<m>/README.md becomes the api/<m> page. Fragments are kept. External http(s):// and mailto: links are untouched.
  4. Search index (emit.go): add one e entry per H2 heading {"p":<page index>,"a":"<id>","h":"<heading text>","x":"<plain text of that section, at most 300 characters>"}. Plain text is extracted from the AST (text nodes only), never from rendered HTML.
  5. Sidebar: the api section lists every module page by name, and llms.txt gets ## API reference with one item per module. Extend docs/site.yaml handling only in code; site.yaml already lists api.
  6. Tests (smoke level; full coverage is plan 11.1-06): in internal/docsite/docsite_test.go add TestSlugIDs (duplicates get -1/-2, punctuation dropped, _ kept) and TestReadmeIngestion over a t.TempDir() fixture (one module with README, one without: the second yields the readme: problem). In cmd/summer/docs_test.go add TestEveryModuleInSidebar (for each discovered modules/<m> with non-test Go files, api/<m>.html exists in a real-tree build and the sidebar HTML of index.html links it) and TestDocsAIOutputsInSync (the set and order of pages from docsite equals: the .html files, the .md siblings, the .md links in llms.txt, and the Source: lines in llms-full.txt; llms.txt line 1 is # SummerCMS, the next non-empty line starts with > , and every H2 is followed by - [ items).

Stage only this task's files. go vet ./internal/docsite/... ./cmd/summer && go test ./internal/docsite ./cmd/summer -run '^(TestSlugIDs|TestReadmeIngestion|TestEveryModuleInSidebar|TestDocsAIOutputsInSync|TestDocsTree)$' -count=1 -v <fails_when>non-zero exit, a "--- FAIL" line, or "no tests to run" in the output</fails_when> <acceptance_criteria> - go test ./cmd/summer -run '^TestEveryModuleInSidebar$' -count=1 -v prints --- PASS: TestEveryModuleInSidebar. - go test ./cmd/summer -run '^TestDocsAIOutputsInSync$' -count=1 -v prints --- PASS: TestDocsAIOutputsInSync. - grep -n 'parser.WithIDs' internal/docsite/render.go finds a match. - After go run ./cmd/summer docs:build --out "$d", ls "$d/api" | grep -c '\.html$' equals the number of modules/* directories that hold a non-test .go file (22 at planning time; derive the number, do not hard-code it). - grep -c '^## API reference$' "$d/llms.txt" prints 1. - grep -o '"a":"[^"]*"' "$d/search-index.json" | head -1 prints a heading anchor entry. </acceptance_criteria> Every module README is an API reference page in the sidebar, llms.txt and search; heading IDs come from one slug implementation; the AI outputs match the page tree in order.

Task 3: Verified snippets: src= extraction, drift detection, `summer docs:sync`, and the first Example in the docs internal/docsite/snippet.go, internal/docsite/docsite.go, internal/docsite/render.go, internal/docsite/docsite_test.go, cmd/summer/docs.go, cmd/summer/main_test.go, modules/bonfire/example_test.go, docs/setup/installation.md, README.md - internal/docsite/docsite.go and internal/docsite/render.go (as written in Tasks 1-2) - modules/bonfire/call_test.go lines 11-36 (fixture shape for bonfire.Call) - `go doc ./modules/bonfire Call` output - .planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-RESEARCH.md section Q4 (src forms and the five checker rules) and "Security Domain" T-11.1-01 - .planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-UI-SPEC.md CLI output rows "Drifted snippet", "Missing snippet source", "docs:sync" - README.md sections "Repository layout" and "Development" Per D-07 (include by reference, embedmd style, chosen in RESEARCH Q4):
  1. snippet.go: ParseSrc(info) splits the fence info string with strings.Fields and returns the src= value split at the first # into Ref{Path, Fragment}. Three forms: no fragment = whole file; #Ident = top-level Go declaration with its doc comment, taken verbatim from token.FileSet offsets, except that an Example* function yields its body dedented with the trailing // Output: comment (as godoc shows it, via go/doc Examples or equivalent); #region = the lines strictly between // docs:start region and // docs:end region (# docs:start region / # docs:end region in YAML), dedented, markers excluded.
  2. Confinement (T-11.1-01), each a snippet: problem at the fence line: path must be relative, clean (no .. after filepath.Clean), resolve inside Root after filepath.EvalSymlinks, not start any segment with . (dotfiles, .env*), and for .go files sit in the root module, meaning no directory between the file and Root holds its own go.mod (this excludes examples/hello/** and admin/**). An Example* fragment without an // Output: comment is a problem (SC4 "run", not just compiled). A #region or #Ident fragment inside a _test.go file must sit in a function that is a Test* or Example* function, or that is called from one in the test files of the same directory (checked with go/ast), so D-07's "compiled and run" holds. A fragment or whole file from non-test Go source must sit in a package directory that has _test.go files.
  3. Drift: for every fence with src= in pages under Src, compare the fence body with Extract after trailing-newline normalisation. Problems use the UI-SPEC wording: {file}:{line}: snippet: body differs from {src} (run: summer docs:sync) and {file}:{line}: snippet: {src} not found. Missing file, ident or region fails Check, so docs:build writes nothing (D-07).
  4. Raw output: in each page's .md, reduce a fence info string to its first word (drop src=). In HTML, render fences with src= inside <figure class="code"> with a <figcaption> showing path#fragment linked to source_url with {path} filled (fragment not sent), then <pre><code> with the escaped body; fences without src= get the same figure without a caption.
  5. Sync(opts): rewrite each drifted fence body in place under Src, preserving everything else byte for byte; report docs:sync: updated {n} snippets in {m} files or docs:sync: all snippets up to date. Add docsSyncCommand() named docs:sync (description "Rewrite src= code blocks from their sources", flags root, src) in cmd/summer/docs.go, register it in toolCommands(), and add it to the TestToolCommandNames want list.
  6. modules/bonfire/example_test.go (package bonfire_test): ExampleCall defines one bonfire.Command named acme:greet with an Arg named name whose Run prints Hello, <name> through out.Printf, calls bonfire.Call(context.Background(), cmds, "acme:greet", []string{"blog"}, os.Stdout), and ends with // Output: Hello, blog. go vet checks the Example name against bonfire.Call.
  7. docs/setup/installation.md: add a "Check your install" section with a go src=modules/bonfire/example_test.go#ExampleCall fence whose body is produced by running go run ./cmd/summer docs:sync (never hand-typed), explaining that console commands are plain bonfire.Command values.
  8. Root README.md: add a docs/ row to the "Repository layout" table ("Documentation source; summer docs:build renders it into a static site.") and a summer docs:build / summer docs:sync line in "Development". Use neutral names only.
  9. Smoke tests in internal/docsite/docsite_test.go: TestSnippetForms (whole file, #Ident, Example body with Output, #region) and TestSnippetConfinement (absolute path, ../x, .env, a path inside a nested-go.mod dir, a symlink escaping the root, an Example without Output, and a region inside a _test.go helper that no Test or Example calls each yield a snippet: problem) over t.TempDir() fixtures; TestSyncRewritesDrift (a drifted fixture fence is rewritten and a second Sync reports up to date).

Stage only this task's files. go vet ./... && go test ./internal/docsite ./cmd/summer ./modules/bonfire -count=1 <fails_when>non-zero exit or a "FAIL" line in the output</fails_when> go test ./modules/bonfire -run '^ExampleCall$' -count=1 -v <fails_when>non-zero exit, no "--- PASS: ExampleCall" line, or "no tests to run"</fails_when> <acceptance_criteria> - grep -n 'src=modules/bonfire/example_test.go#ExampleCall' docs/setup/installation.md finds a match. - grep -n '// Output: Hello, blog' modules/bonfire/example_test.go finds a match. - go run ./cmd/summer docs:sync prints docs:sync: all snippets up to date on a clean tree. - Appending a character inside the installation page's Go fence body in a scratch copy of docs/ and running go run ./cmd/summer docs:build --check --src <scratch-docs> exits non-zero and prints snippet: body differs from modules/bonfire/example_test.go#ExampleCall (run: summer docs:sync). - go test ./internal/docsite -run '^(TestSnippetForms|TestSnippetConfinement|TestSyncRewritesDrift)$' -count=1 -v prints three --- PASS lines. - grep -n '"docs:sync"' cmd/summer/main_test.go finds a match. - grep -n 'docs:build' README.md finds a match. </acceptance_criteria> Go fences in docs are verified copies of compiled, running source; drift or a missing source fails go test and the build; summer docs:sync repairs copies; the first Example runs under go test.

<threat_model>

Trust Boundaries

Boundary Description
docs/ Markdown and module READMEs → generator Repository content becomes public HTML; authors are trusted but mistakes and pasted HTML are expected
src= references → repository files A fence can name any path; the generator must not publish files outside the intended source set
generator → filesystem (--out) The build deletes and rewrites a directory named on the command line

STRIDE Threat Register

Threat ID Category Component Severity Disposition Mitigation Plan
T-11.1-01 Information disclosure internal/docsite/snippet.go src= resolution high mitigate Relative, cleaned, EvalSymlinks-confined paths; dot-segments (dotfiles, .env) and nested-go.mod dirs refused; TestSnippetConfinement plants each escape
T-11.1-02 Tampering (XSS) internal/docsite/render.go goldmark renderer high mitigate goldmark raw-HTML option left off, so HTML in Markdown is escaped; chrome rendered by html/template with contextual escaping; the unsafe option is grep-gated absent
T-11.1-03 Tampering (data loss) internal/docsite Build output guard high mitigate Refuse --out equal to root, inside or containing --src; clean only a dir holding the .summer-docs marker; nothing written when problems exist
T-11.1-04 Tampering docs/site.yaml and frontmatter decoding low mitigate goccy/go-yaml DisallowUnknownField; every field required; typos fail as frontmatter problems
T-11.1-SC Tampering npm/pip/cargo/go installs high accept This plan adds no module or package (goldmark and goccy/go-yaml are already in go.mod); the only new dependency of the phase is gated in plan 11.1-02
</threat_model>
- `go vet ./... && go test ./internal/docsite ./cmd/summer ./modules/bonfire -count=1` green. - `go run ./cmd/summer docs:build --out "$(mktemp -d)/site"` writes pages; `go run ./cmd/summer docs:sync` reports up to date. - `git diff --name-only 9033d81 -- go.mod` prints nothing (no dependency change in this plan).

<success_criteria>

  • SC1 (partial): docs/ holds frontmatter pages grouped by site.yaml sections; every module reachable from the sidebar through its api page.
  • SC2 (partial): a summer CLI command builds a self-contained static site with no Node toolchain.
  • SC3: llms.txt, llms-full.txt and a clean .md per page are emitted and TestDocsAIOutputsInSync asserts they match the page tree.
  • SC4 (partial): the one Go example in docs is a src= copy of a running Example; drift fails go test. </success_criteria>

Artifacts this phase produces

  • Package internal/docsite: Options, Problem (String), Result, SyncResult, Check, Build, Sync, Site, Section, Frontmatter, Page, Ref, ParseSrc, Extract, MarkerFile; embedded theme/templates/page.html, theme/assets/site.css.
  • CLI commands: summer docs:build (flags --root, --src, --out, --base-url, --check), summer docs:sync (flags --root, --src).
  • Example: bonfire.ExampleCall in modules/bonfire/example_test.go.
  • Files: docs/site.yaml, docs/index.md, docs/setup/installation.md, cmd/summer/docs.go, cmd/summer/docs_test.go, internal/docsite/*.go, .gitignore /site/ entry, README.md layout and development lines.
  • Output files per build: <section>/<slug>.html, <section>/<slug>.md, index.html, index.md, api/<module>.html|.md, llms.txt, llms-full.txt, search-index.json, assets/site.css, .summer-docs.
  • Tests: TestDocsTree, TestDocsBuildRealTree, TestEveryModuleInSidebar, TestDocsAIOutputsInSync (cmd/summer); TestSlugIDs, TestReadmeIngestion, TestSnippetForms, TestSnippetConfinement, TestSyncRewritesDrift (internal/docsite).
Create `.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-01-SUMMARY.md` when done