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.
326 lines
35 KiB
Markdown
326 lines
35 KiB
Markdown
---
|
|
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 `# <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: "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>
|