carlos / platform Public

spec: the platform changelog page (carlosframework.com/platform/changelog)

Rulings from Paul 2026-10-06: a change is a merged branch, every release
back to v0.1.0, change lines are records, the join runs here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev fb26eebc78fb07998dae836f79f5c419a24be03c parent 1c43c2c
2 files changed, +296 −0
  • docs/superpowers/specs/2026-10-06-platform-changelog-design.md +257 −0
  • docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md +39 −0

CI passed — run details

diff --git a/docs/superpowers/specs/2026-10-06-platform-changelog-design.md b/docs/superpowers/specs/2026-10-06-platform-changelog-design.md
new file mode 100644
index 000000000..e5c7fdd17
--- /dev/null
+++ b/docs/superpowers/specs/2026-10-06-platform-changelog-design.md
@@ -0,0 +1,257 @@
+# 🤖 The platform changelog (carlosframework.com/platform/changelog)
+
+Status: design, 2026-10-06. Asked for by Paul the same day: "a page which
+lists every change for every release, links to docs, links to the Amadan
+branch/discussion and shows who did the work".
+
+## Rulings (Paul, 2026-10-06; not to reopen)
+
+1. **A change is a merged branch.** Each release lists every first-parent
+ commit that landed on `main` since the previous tag, which since v0.20.0
+ is one merged branch each. Groundwork, docs and fixes all count. The
+ release's published notes sit on top as the summary.
+2. **Every release, back to v0.1.0, with a sliding link depth.** From the
+ Amadan era on, a change links to its Amadan branch and discussions.
+ Before that it links to its commit on Amadan (public) and credits the git
+ authors. The GitHub mirror is private, so no GitHub PR links.
+3. **Change lines are records, not copy.** A change shows its branch name
+ and squash subject verbatim, styled as data, under a visible 🤖. Only
+ the page's own framing and the already-reviewed release notes are prose.
+ The framing goes through copy review.
+4. **The join runs in the platform repo.** A generator writes a committed
+ `docs/site/changelog.json`; the website vendors it with the docs corpus
+ and renders it at build time. No network at website build.
+
+## What exists (verified 2026-10-06)
+
+- 28 tags, v0.1.0 (2026-08-02) to v0.24.2. v0.2.0 and v0.22.1 were tagged
+ but never published on `carlosframework/releases`.
+- Merge shapes on `main`'s first-parent line: amadan squashes ("Squashed
+ from X into main." in the body), amadan merges ("Merge branch 'X' into
+ main"), GitHub merges ("Merge pull request #N from carlosframework/X"),
+ and before those, direct commits. All 55 first-parent commits since
+ v0.20.0 name their branch.
+- Amadan keeps a tombstone ref for each merged branch at
+ `refs/amadan/closed/<branch>` (151 today), so a squashed branch's own
+ commits stay reachable. Its page is `https://amadan.net/carlos/platform/-/branch/<url-escaped name>`;
+ a commit is `https://amadan.net/carlos/platform/commit/<sha>`; a
+ discussion is `https://amadan.net/carlos/platform/-/discussion/<id>`.
+- `amadan discuss show carlos/platform <id> -json` and `amadan discuss
+ list carlos/platform -branch <b> -state all -json` read discussions. The
+ unfiltered list caps at 50. There is no read for a branch's description.
+- Authors: people (`Paul Campbell`, `Bill Horsman`, `James Adam`, `Tony
+ Wood`, under several emails) and agents whose email carries their
+ operator (`claude~paul@keymail.dev`, `codex~paul@keymail.dev`,
+ `claude-code~bill@keymail.dev`). Older person-authored commits carry
+ `Co-Authored-By: Claude …` trailers.
+- Release notes changed shape. v0.19.0 on: a `carlos vX (platform …)` line,
+ `- **Bold lead.** …` bullets, an operator section (`**For operators**`,
+ `**Before you roll**`, `**Before you roll a console to this release**`),
+ and a closing "Verify a download…" line. Before v0.19.0: free markdown
+ with `##` headings, tables, fences, an `## Install` section and a
+ "🤖 Generated with Claude Code" line.
+- carlosframework.com is `carlosframework/website` (Eleventy, plain CSS, no
+ browser JS but the AI disclaimer). `hack/sync-docs.mjs` vendors
+ `docs/site/` from a platform checkout; `hack/check-docs.mjs` checks
+ the build's internal links. Its rule: every AI-written page carries a
+ visible 🤖, and one on the top heading covers the page.
+
+## The data: `docs/site/changelog.json`
+
+```json
+{
+ "//": "generated by go run ./cmd/changelog; do not edit",
+ "repo": "https://amadan.net/carlos/platform",
+ "releases": [
+ {
+ "tag": "v0.24.2",
+ "date": "2026-10-06",
+ "sha": "7f403a77…",
+ "published": true,
+ "notes_url": "https://github.com/carlosframework/releases/releases/tag/v0.24.2",
+ "notes": {
+ "form": "bullets",
+ "sections": [
+ {"title": "", "bullets": [
+ {"md": "**CI can deploy through the console.** …",
+ "branches": ["design-deploy-credential"],
+ "docs": ["/docs/cli#carlos-services"]}
+ ]},
+ {"title": "For operators", "bullets": [ … ]}
+ ]
+ },
+ "changes": [
+ {
+ "sha": "d1dab9a9…",
+ "date": "2026-10-06",
+ "subject": "deploy-credential: a rung-scoped deploy credential so CI promotes through the console (#229)",
+ "branch": "design-deploy-credential",
+ "branch_url": "https://amadan.net/carlos/platform/-/branch/design-deploy-credential",
+ "commits": 14,
+ "discussions": [{"id": 229, "title": "CI cannot promote …", "state": "closed"}],
+ "docs": ["/docs/cli#carlos-services"],
+ "credit": [{"person": "Paul Campbell", "agents": ["claude"]}]
+ }
+ ]
+ }
+ ]
+}
+```
+
+- `notes.form` is `"bullets"` for the v0.19.0-on shape and `"markdown"`
+ otherwise. A markdown body is kept whole as `notes.md`, with headings
+ demoted two levels (the release is the page's `h2`) and the
+ boilerplate removed: the `carlos vX (platform …)` line, the "Verify a
+ download" line, an `## Install` section, and the "Generated with Claude
+ Code" line. Unpublished tags have `"notes": null`.
+- Bullet sections keep the notes' own operator heading text, minus the
+ bold markers.
+- `subject` is the commit's subject line. `branch_url` is set only when
+ the branch has an Amadan record (a tombstone ref, a live head, or a
+ squash/merge made on Amadan). A GitHub-era branch keeps its `branch`
+ name with no link.
+- No email address ever appears in the file. The generator fails if one
+ would.
+
+## The generator: `cmd/changelog`, logic in `internal/changelog`
+
+`go run ./cmd/changelog [-out docs/site/changelog.json] [-since <tag>]`
+
+Run from a full checkout after `git fetch origin --tags
+'+refs/amadan/closed/*:refs/amadan/closed/*'`. It needs `gh` (release
+bodies) and `amadan` (discussions) signed in. It writes the file and
+prints one summary line per release.
+
+**Inputs, each behind an interface** so tests run on fakes and a scratch
+git repo:
+
+- `Git`: tags with dates, first-parent commits in a range, a commit's
+ message, parents and changed `docs/site/*.md` hunks, and the commits
+ reachable from a tombstone but not from `main`.
+- `Releases`: release body by tag, or not-found.
+- `Discussions`: one by id, and the list for a branch.
+
+**Ranges.** Tags sort by semver. Release N covers
+`git rev-list --first-parent <tag N-1>..<tag N>`; v0.1.0 covers
+everything up to it.
+
+**Branch name**, from the first match in the message: `Squashed from (\S+)
+into`, `Merge branch '([^']+)'`, `Merge pull request #\d+ from
+[^/\s]+/(\S+)`. None: a direct commit, `branch` empty.
+
+**The branch's own commits**, for credit and discussion references:
+
+- a two-parent merge: `<first parent>..<second parent>`;
+- a squash with a tombstone `refs/amadan/closed/<branch>`:
+ `<squash>^..<tombstone>`, non-merge commits only;
+- otherwise the commit itself.
+
+**Credit.** For each own commit, read the author and every
+`Co-Authored-By` trailer. An identity resolves to a person, an agent, or
+both:
+
+- `<agent>~<keymail>`: agent `<agent>` (`claude-code` reads `claude`),
+ person = `people[<keymail>]`.
+- `noreply@anthropic.com` or a name starting `Claude`: agent `claude`, no
+ person. `codex` names: agent `codex`.
+- Anything else: person = `people[<email>]`.
+- `noreply@github.com` and `ledger@amadan` are skipped.
+
+`docs/site/people.json` maps every email and keymail to a display name.
+An identity with no entry fails the run and names the address, so a new
+contributor is a one-line decision, not a leaked email. Agent-only
+identities (no person) attach to the commit's author's person when the
+author is a person, so "Paul Campbell with claude" survives a
+`Co-Authored-By` trailer. Credit groups by person, ordered by commit
+count, agents listed per person; an agent with no person anywhere is its
+own entry.
+
+**Discussions.** References come from the squash message and the own
+commits' messages: `#(\d+)`, `discussion (\d+)`, `amadan #(\d+)`. A
+number counts only when `discuss show` finds it and it was created before
+the commit, which rejects GitHub-era issue and PR numbers. Plus every
+discussion `discuss list -branch <branch>` returns. Deduped, sorted by id.
+Each lookup is cached for the run.
+
+**Docs links.** For a change: each `docs/site/<page>.md` the commit (the
+squash, or the merge's diff against its first parent) adds lines to,
+mapped to the nearest enclosing heading in the new file, then kept only
+if that heading still exists in today's corpus (`docsite.Load`, anchors
+by `docsite.Anchor`). A page that no longer exists drops out. For a
+release-note bullet: each inline code span starting `carlos <verb>` whose
+longest matching CLI heading (`carlos <verb>`, `carlos <verb> <sub>`) exists
+on the CLI page. Deduped, at most five per change.
+
+**Bullet to branch links.** `docs/site/changelog-links.json`, hand-kept:
+
+```json
+{"v0.24.2": {"CI can deploy through the console.": ["design-deploy-credential"]}}
+```
+
+keyed by the bullet's bold lead. The generator fails on a key that
+matches no bullet in that release, or a branch that is not one of that
+release's changes. Bullets without an entry have no branch links. The
+backfill covers v0.19.0 on; older notes are markdown and get none.
+
+## The page: `/platform/changelog/`
+
+In `carlosframework/website`:
+
+- `hack/sync-docs.mjs` vendors `docs/site/changelog.json` as
+ `src/_data/docschangelog.json` with the usual `"//"` notice (the sync
+ already owns `src/_data/docs*.json`).
+- `src/changelog.njk` with `permalink: /platform/changelog/index.html`,
+ in the platform page's world: its masthead, the platform blue accent,
+ `site.css`, a page-local `<style>` only where `site.css` has nothing.
+- The top heading carries the visible 🤖, which covers the page. A short
+ intro (copy-reviewed) says what a change is, that lines are verbatim
+ records, and how far back links go.
+- One `<section id="v0-24-2">` per release, newest first: the tag as `h2`
+ with a self link, date, a link to the published notes (or "not
+ published"), then the notes (bullets with their branch and docs links;
+ the operator section in a `<details>`; markdown notes rendered by the
+ site's markdown-it), then "Changes (N)" as a list. Each change: branch
+ name (linked when it has a record) or short SHA for a direct commit,
+ the subject in mono, discussions as `#229` links titled with the
+ discussion title, docs links, the credit ("Paul Campbell with claude"),
+ and the short SHA linked to its commit.
+- A release with more than 25 changes shows the first 25 and the rest in
+ a `<details>`. No script.
+- `/platform/` gets one link to the changelog (copy-reviewed).
+- `hack/check-docs.mjs` also checks that the page built, that every
+ release has its section id, and that every `/docs` link on it
+ resolves, using the code it already has for docs pages.
+
+## Gates and upkeep
+
+- `internal/docsite` gains `changelog_test.go`: the file parses; tags are
+ unique and semver-ordered newest first; every `/docs` link resolves in
+ the corpus; every `branch_url` and discussion link is on the repo URL;
+ no address from `people.json` and nothing shaped `<x>~<y>@` appears
+ anywhere in the file; every `changelog-links.json` key
+ matched (the generator already enforces it, and the test re-checks the
+ committed file). The test does not need git tags or network, so it runs
+ in CI as is.
+- `internal/changelog` has unit tests for each parser and resolver on
+ fakes, and one test on a scratch git repo that builds tags, a squash with
+ a tombstone, a two-parent merge and a direct commit, and checks the whole
+ output.
+- The README's release recipe gains a step after Publish: regenerate the
+ changelog, add the new release's bullet links, commit on a branch, merge,
+ then re-sync and ship the website. (Copy-reviewed.)
+
+## Asks of Amadan (filed, not worked around)
+
+1. A read for a branch's metadata, including merged ones: description,
+ landed SHA, who pushed. Until then a change's line is its squash
+ subject.
+2. Paging on `discuss list` (it caps at 50). The generator does not need
+ it: it reads by id and by branch.
+
+## Out of scope
+
+- An RSS or Atom feed. It's easy to add from the same data later.
+- Per-change prose summaries (ruling 3).
+- Changing how release notes are written.
+- Rolling or deploying anything beyond the website's own ship and
+ promote, which waits for the copy review.
diff --git a/docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md
new file mode 100644
index 000000000..d48a2e7b6
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-REVIEW-BRIEF-CHANGELOG.md
@@ -0,0 +1,39 @@
+# 🤖 Review brief: the platform changelog spec
+
+You are reviewing `docs/superpowers/specs/2026-10-06-platform-changelog-design.md`
+in this repo (carlos/platform), at the commit you are run on. You are the
+independent second reader; the author is Claude.
+
+The website it renders on is a separate checkout at
+`/home/paulca/github.com/carlosframework/website` (read `AGENTS.md`,
+`eleventy.config.js`, `hack/sync-docs.mjs`, `hack/check-docs.mjs` there).
+
+## Fixed: do not reopen
+
+The four rulings at the top of the spec are Paul's. Report only where the
+mechanism fails to honour them, not whether they are right.
+
+## What to look for
+
+1. **Wrong facts.** Check every claim in "What exists" against the repo:
+ run `git log --first-parent`, `git for-each-ref refs/amadan/closed`
+ (may need `git fetch origin '+refs/amadan/closed/*:refs/amadan/closed/*'`
+ first; if you cannot fetch, say so), and read `internal/docsite`.
+2. **Mechanisms that produce wrong output.** Branch-name parsing, the
+ own-commit ranges (squash with tombstone, two-parent merge, direct),
+ credit resolution, discussion-number filtering, docs-link derivation,
+ notes parsing across both shapes. Name a concrete commit or tag in
+ this repo where the rule gives a wrong or misleading line.
+3. **Privacy and accuracy on a public page.** Anything that could publish
+ an email address, a private repo link, or credit the wrong person.
+4. **Gaps the gate misses.** What could be wrong in the committed JSON
+ and still pass `changelog_test.go` and the website's check.
+5. **Scope.** Anything that should be cut.
+
+## Output
+
+A numbered list of findings, each with severity (P1 breaks the page or
+publishes something wrong; P2 misleading or fragile; P3 nit), the spec
+line it is about, the evidence (command and output, or file:line), and a
+proposed fix. Then a one-line verdict: accept, accept with fixes, or
+reject. Do not edit any file.