# 🤖 Platform changelog implementation plan

> **For agentic workers:** executed natively by the session that wrote it
> (Paul, 2026-10-06: "work overnight until this is done"), with an Astra
> whole-branch review at the end. Steps use checkbox syntax.

**Goal:** `docs/site/changelog.json` generated from git, release notes and
Amadan, gated in CI, and rendered at carlosframework.com/platform/changelog.

**Architecture:** `internal/changelog` holds pure parsers and resolvers
plus a `Build` that drives three source interfaces (`Git`, `Releases`,
`Discussions`); `cmd/changelog` wires the real sources (git, gh, amadan)
and writes the file. `internal/docsite` gates the committed file. The
website vendors it through `hack/sync-docs.mjs` and renders it with one
Nunjucks template.

**Tech stack:** Go 1.26 stdlib; Eleventy 3 + markdown-it (website).

**Spec:** `docs/superpowers/specs/2026-10-06-platform-changelog-design.md`

## Global constraints

- No email address in `changelog.json` or on the page. Unknown identity = generator failure.
- No GitHub PR links (the mirror is private). Links only to amadan.net, github.com/carlosframework/releases and /docs.
- Change lines are verbatim records; the page carries one visible 🤖 on its h1.
- Website: no browser JS beyond the existing disclaimer; light and dark both work; no em dashes in new copy.
- Copy review gates the page's framing strings and the README step before anything ships.
- Tests: `export GOCACHE=$TMPDIR/gocache GOFLAGS=-mod=mod GOPROXY=off`, run `nice -n 19`, package by path, never `./...`.

## Review focus

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.
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.
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.
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.
5. A heading in old notes (`## carlos deploy`) inside a markdown body: demoted to `####` so the page outline stays intact. Test.

---

### Task 1: identities and credit (`internal/changelog/credit.go`)

- Create `credit.go`, `credit_test.go`.
- 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 }`.
- 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.

### Task 2: messages, ranges and notes (`branch.go`, `notes.go`, `refs.go`)

- `func BranchName(message string) string` (three patterns, first match).
- `func DiscussionRefs(message string) []int` (`#N`, `discussion N`, `amadan #N`; deduped, sorted).
- `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.
- `func CLIDocLinks(md string, cliHeadings map[string]string) []string` (inline code `carlos …` → longest heading match → `/docs/cli#anchor`).
- Tests per Review Focus 3 and 5, plus each operator-heading variant and the v0.1.0 one-paragraph body.

### Task 3: Build on fakes and on a scratch repo (`build.go`)

- 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) }`.
- `func Build(src Sources, people People, links Links, site *docsite.Site) (*Changelog, error)`.
- Docs links from hunks: nearest heading at or above each added line, kept only if `site` has that page and anchor.
- Email guard over the marshalled output.
- `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.

### Task 4: `cmd/changelog`, people.json, real sources

- `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")`.
- `docs/site/people.json` from the identities found in history (names only from git's own author names).
- Run it; read the output for every release; fix what reads wrong.

### Task 5: bullet links backfill (`docs/site/changelog-links.json`)

- For v0.19.0 on, map each bullet's lead to the branches behind it by reading each change's subject and diff. Regenerate.

### Task 6: the gate (`internal/docsite/changelog_test.go`)

- 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.

### Task 7: website

- 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.

### Task 8: README release step, copy review, Astra code review, merge

- 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.
