| 1 | # π€ Platform changelog implementation plan |
| 2 | |
| 3 | > **For agentic workers:** executed natively by the session that wrote it |
| 4 | > (Paul, 2026-10-06: "work overnight until this is done"), with an Astra |
| 5 | > whole-branch review at the end. Steps use checkbox syntax. |
| 6 | |
| 7 | **Goal:** `docs/site/changelog.json` generated from git, release notes and |
| 8 | Amadan, gated in CI, and rendered at carlosframework.com/platform/changelog. |
| 9 | |
| 10 | **Architecture:** `internal/changelog` holds pure parsers and resolvers |
| 11 | plus a `Build` that drives three source interfaces (`Git`, `Releases`, |
| 12 | `Discussions`); `cmd/changelog` wires the real sources (git, gh, amadan) |
| 13 | and writes the file. `internal/docsite` gates the committed file. The |
| 14 | website vendors it through `hack/sync-docs.mjs` and renders it with one |
| 15 | Nunjucks template. |
| 16 | |
| 17 | **Tech stack:** Go 1.26 stdlib; Eleventy 3 + markdown-it (website). |
| 18 | |
| 19 | **Spec:** `docs/superpowers/specs/2026-10-06-platform-changelog-design.md` |
| 20 | |
| 21 | ## Global constraints |
| 22 | |
| 23 | - No email address in `changelog.json` or on the page. Unknown identity = generator failure. |
| 24 | - No GitHub PR links (the mirror is private). Links only to amadan.net, github.com/carlosframework/releases and /docs. |
| 25 | - Change lines are verbatim records; the page carries one visible π€ on its h1. |
| 26 | - Website: no browser JS beyond the existing disclaimer; light and dark both work; no em dashes in new copy. |
| 27 | - Copy review gates the page's framing strings and the README step before anything ships. |
| 28 | - Tests: `export GOCACHE=$TMPDIR/gocache GOFLAGS=-mod=mod GOPROXY=off`, run `nice -n 19`, package by path, never `./...`. |
| 29 | |
| 30 | ## Review focus |
| 31 | |
| 32 | 1. A squash whose branch was later re-used by name (two tombstones over time): own-commit range must stop at the squash's parent, never pull in later work. Test: tombstone range is `<squash>^..<tombstone>` filtered to commits not on main. |
| 33 | 2. A `#N` in a GitHub-era message that collides with a live Amadan id: rejected by the created-before-commit rule. Test with a fake discussion created after the commit. |
| 34 | 3. A release-note bullet containing an email-like string (`bill@`): must not trip the email guard unless it is a full address from people.json or a keymail. Test both. |
| 35 | 4. A tag with zero first-parent commits in range (v0.24.1 had one, a re-tag could have none): renders "No changes" rather than failing. Test. |
| 36 | 5. A heading in old notes (`## carlos deploy`) inside a markdown body: demoted to `####` so the page outline stays intact. Test. |
| 37 | |
| 38 | --- |
| 39 | |
| 40 | ### Task 1: identities and credit (`internal/changelog/credit.go`) |
| 41 | |
| 42 | - Create `credit.go`, `credit_test.go`. |
| 43 | - Produces: `type People map[string]string` (email or keymail β display name, keys lower-cased); `type Credit struct{ Person string `json:"person,omitempty"`; Agents []string `json:"agents,omitempty"` }`; `func ParseIdentity(name, email string, people People) (person, agent string, skip bool, err error)`; `func CoAuthors(message string) [][2]string` (name, email from `Co-Authored-By:` trailers, case-insensitive); `func Tally(commits []CommitIdent, people People) ([]Credit, error)` where `type CommitIdent struct{ AuthorName, AuthorEmail, Message string }`. |
| 44 | - Rules from the spec's Credit paragraph. Tests: keymail agent with known person; `claude-code` β `claude`; `noreply@anthropic.com`; codex name; plain person by email; unknown email β error naming it; GitHub/ledger skipped; agent-only trailer attaches to a person author; ordering by count; an agent with no person anywhere stands alone. |
| 45 | |
| 46 | ### Task 2: messages, ranges and notes (`branch.go`, `notes.go`, `refs.go`) |
| 47 | |
| 48 | - `func BranchName(message string) string` (three patterns, first match). |
| 49 | - `func DiscussionRefs(message string) []int` (`#N`, `discussion N`, `amadan #N`; deduped, sorted). |
| 50 | - `type Notes struct{ Form string; Sections []NoteSection; MD string }`, `type NoteSection struct{ Title string; Bullets []Bullet }`, `type Bullet struct{ MD string; Lead string; Branches []string; Docs []string }`; `func ParseNotes(body string) *Notes` (bullets form when a `- **` bullet follows a `carlos vX (platform` line; else markdown form with boilerplate stripped and headings demoted by two, capped at h6). `Lead` = text inside the first `**β¦**` of a bullet. |
| 51 | - `func CLIDocLinks(md string, cliHeadings map[string]string) []string` (inline code `carlos β¦` β longest heading match β `/docs/cli#anchor`). |
| 52 | - Tests per Review Focus 3 and 5, plus each operator-heading variant and the v0.1.0 one-paragraph body. |
| 53 | |
| 54 | ### Task 3: Build on fakes and on a scratch repo (`build.go`) |
| 55 | |
| 56 | - Interfaces: `Git{ Tags() ([]Tag, error); FirstParent(from, to string) ([]Commit, error); Range(from, to string) ([]Commit, error); OnMain(sha string) bool; Tombstone(branch string) (string, bool); DocsHunks(sha string) ([]DocsHunk, error); Branches() map[string]bool }`, `Releases{ Body(tag string) (string, bool, error) }`, `Discussions{ Show(id int) (*Discussion, error); ForBranch(b string) ([]Discussion, error) }`. |
| 57 | - `func Build(src Sources, people People, links Links, site *docsite.Site) (*Changelog, error)`. |
| 58 | - Docs links from hunks: nearest heading at or above each added line, kept only if `site` has that page and anchor. |
| 59 | - Email guard over the marshalled output. |
| 60 | - `gitexec.go`: the real `Git` over `git` subprocesses; scratch-repo test builds v0.1.0, a direct commit, a two-parent merge, a squash with a `refs/amadan/closed/x` tombstone, and tag v0.2.0, then checks ranges, branches, credit and docs links. |
| 61 | |
| 62 | ### Task 4: `cmd/changelog`, people.json, real sources |
| 63 | |
| 64 | - `cmd/changelog/main.go`: flags `-out`, `-repo` (default `carlos/platform`); real `Releases` over `gh release view <tag> -R carlosframework/releases --json body`, real `Discussions` over `amadan discuss show|list β¦ -json`; loads `docs/site/people.json`, `docs/site/changelog-links.json` (missing = empty), `docsite.Load("docs/site")`. |
| 65 | - `docs/site/people.json` from the identities found in history (names only from git's own author names). |
| 66 | - Run it; read the output for every release; fix what reads wrong. |
| 67 | |
| 68 | ### Task 5: bullet links backfill (`docs/site/changelog-links.json`) |
| 69 | |
| 70 | - For v0.19.0 on, map each bullet's lead to the branches behind it by reading each change's subject and diff. Regenerate. |
| 71 | |
| 72 | ### Task 6: the gate (`internal/docsite/changelog_test.go`) |
| 73 | |
| 74 | - Checks from the spec's "Gates and upkeep". Mutation check: break one docs anchor and one email in a temp copy and watch each fail. |
| 75 | |
| 76 | ### Task 7: website |
| 77 | |
| 78 | - In a worktree of `carlosframework/website` on branch `platform-changelog`: `hack/sync-docs.mjs` also vendors `changelog.json` β `src/_data/docschangelog.json` (and `--check` compares it); `src/changelog.njk`; a markdown filter for notes; `check-docs.mjs` checks the page; a link from `src/platform/index.html`. `npm run check`, then look at it in a browser at both colour schemes. |
| 79 | |
| 80 | ### Task 8: README release step, copy review, Astra code review, merge |
| 81 | |
| 82 | - README "Releasing the platform" step 10. Copy review of the page framing and the README step. Astra review of both branches; fix; `amadan branch merge`; website PR (GitHub) left for Paul unless copy approved. |
| 83 | |