--- phase: 11.1-summercms-documentation-for-humans-and-ai-agents plan: 01 type: execute wave: 1 depends_on: [] files_modified: - 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 autonomous: true requirements: [DOCS-01, DOCS-02, DOCS-03, DOCS-04] assumption_delta_decision: no-change user_setup: [] estimate: tokens: 95000 raw_tokens: 95000 tasks: 3 confidence: low must_haves: truths: - "Per D-01, every page under docs/ is Markdown that starts with YAML frontmatter (title, description, section, order) followed by a first body line `# `, 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: "llms.txt and the per-page .md files are useful to an AI agent reading raw files (spec shape, predictable URLs)." verification: backstop prohibitions: - requirement_id: DOCS-02 category: privacy status: resolved verification: judgment resolution: "Theme assets are embedded and emitted under /assets/; templates reference only base_url-prefixed paths." reason: "Framework docs are served to developers; a third-party request leaks visitor data and breaks offline preview." statement: "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." artifacts: - path: "internal/docsite/docsite.go" provides: "Options, Problem, Result, SyncResult, Build, Check, Sync entry points" contains: "func Build(" - path: "internal/docsite/load.go" provides: "strict site.yaml and frontmatter decoding, page walk, README ingestion" contains: "DisallowUnknownField" - path: "internal/docsite/render.go" provides: "goldmark pipeline with GFM, shared slug IDs and AST transformers" contains: "parser.WithIDs" - path: "internal/docsite/emit.go" provides: "html, .md, llms.txt, llms-full.txt and search-index.json emission" contains: "llms-full.txt" - path: "internal/docsite/snippet.go" provides: "src= parsing, confinement, extraction and drift detection" contains: "docs:start" - path: "cmd/summer/docs.go" provides: "summer docs:build and docs:sync commands" contains: "docs:build" - path: "modules/bonfire/example_test.go" provides: "first verified Example referenced by docs" contains: "// Output:" - path: "docs/setup/installation.md" provides: "first guide page with a src= snippet" contains: "src=modules/bonfire/example_test.go#" key_links: - from: "cmd/summer/docs.go" to: "internal/docsite/docsite.go" via: "docs:build calls docsite.Build, docs:sync calls docsite.Sync" pattern: "docsite\\.(Build|Sync)" - from: "internal/docsite/render.go" to: "github.com/yuin/goldmark" via: "parse context built with parser.WithIDs(slug IDs)" pattern: "parser\\.WithIDs" - from: "docs/setup/installation.md" to: "modules/bonfire/example_test.go" via: "go fence info string src= reference kept in sync by the snippet check" pattern: "src=modules/bonfire/example_test\\.go#Example" - from: "internal/docsite/load.go" to: "modules/*/README.md" via: "dynamic module discovery ingests each README as an api page" pattern: "README\\.md" --- <objective> 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`. </objective> <execution_context> @~/.claude/gsd-core/workflows/execute-plan.md @~/.claude/gsd-core/templates/summary.md </execution_context> <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 <interfaces> 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. </interfaces> </context> <tasks> <task type="tracer"> <name>Task 1: Tracer: `summer docs:build` turns docs/index.md and docs/setup/installation.md into a site with every output</name> <files>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</files> <read_first> - 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) </read_first> <action> 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. </action> <verify> <automated>go vet ./internal/docsite/... ./cmd/summer && go test ./internal/docsite ./cmd/summer -run '^(TestDocsTree|TestDocsBuildRealTree|TestToolCommandNames)$' -count=1 -v</automated> <fails_when>non-zero exit, a "--- FAIL" line, or "no tests to run" in the output</fails_when> </verify> <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> <done>`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.</done> </task> <task type="auto"> <name>Task 2: Every framework module appears as an API reference page, with shared heading IDs, rewritten links and heading-level search entries</name> <files>internal/docsite/load.go, internal/docsite/render.go, internal/docsite/emit.go, internal/docsite/docsite_test.go, cmd/summer/docs_test.go</files> <read_first> - 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 </read_first> <action> 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. </action> <verify> <automated>go vet ./internal/docsite/... ./cmd/summer && go test ./internal/docsite ./cmd/summer -run '^(TestSlugIDs|TestReadmeIngestion|TestEveryModuleInSidebar|TestDocsAIOutputsInSync|TestDocsTree)$' -count=1 -v</automated> <fails_when>non-zero exit, a "--- FAIL" line, or "no tests to run" in the output</fails_when> </verify> <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> <done>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.</done> </task> <task type="auto"> <name>Task 3: Verified snippets: src= extraction, drift detection, `summer docs:sync`, and the first Example in the docs</name> <files>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</files> <read_first> - 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" </read_first> <action> 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. </action> <verify> <automated>go vet ./... && go test ./internal/docsite ./cmd/summer ./modules/bonfire -count=1</automated> <fails_when>non-zero exit or a "FAIL" line in the output</fails_when> <automated>go test ./modules/bonfire -run '^ExampleCall$' -count=1 -v</automated> <fails_when>non-zero exit, no "--- PASS: ExampleCall" line, or "no tests to run"</fails_when> </verify> <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> <done>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.</done> </task> </tasks> <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> <verification> - `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). </verification> <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). <output> Create `.planning/phases/11.1-summercms-documentation-for-humans-and-ai-agents/11.1-01-SUMMARY.md` when done </output>