spec rev 2: the platform changelog, with Astra's four findings and three from the first run
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 files changed,
+77
−20
docs/superpowers/specs/2026-10-06-platform-changelog-design.md+54 −20docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-1.md+23 −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| index e5c7fdd17..46dee6fb3 100644 |
| --- a/docs/superpowers/specs/2026-10-06-platform-changelog-design.md |
| +++ b/docs/superpowers/specs/2026-10-06-platform-changelog-design.md |
| @@ -1,6 +1,9 @@ |
| # 🤖 The platform changelog (carlosframework.com/platform/changelog) |
| -Status: design, 2026-10-06. Asked for by Paul the same day: "a page which |
| +Status: rev 2, 2026-10-06. Rev 1 had Astra's review (accept with fixes, |
| +`CODEX-FINDINGS-CHANGELOG-1.md`); every finding is taken and marked |
| +**(astra N)**. The first real run over all 28 tags added three more, |
| +marked **(run)**. 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". |
| @@ -110,12 +113,17 @@ branch/discussion and shows who did the work". |
| 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. |
| +- No email address and no link into a private repo ever appears in the |
| + file **(astra 3)**. Commit subjects and discussion titles are quoted |
| + with any address replaced by `[address]` and any private-repo URL by |
| + `[private repo]`. Discussion #147's title names a customer's address, |
| + and a 2026-08-03 sync merge names the private mirror. Anything left, |
| + in any field, fails the run: `CheckPublic`, shared by the generator |
| + and the gate. |
| ## The generator: `cmd/changelog`, logic in `internal/changelog` |
| -`go run ./cmd/changelog [-out docs/site/changelog.json] [-since <tag>]` |
| +`go run ./cmd/changelog [-out docs/site/changelog.json]` |
| Run from a full checkout after `git fetch origin --tags |
| '+refs/amadan/closed/*:refs/amadan/closed/*'`. It needs `gh` (release |
| @@ -131,9 +139,13 @@ git repo: |
| - `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. |
| +**Ranges (run).** Tags sort by semver. A published release covers the |
| +first-parent commits no earlier *published* release reaches; an |
| +unpublished tag covers those no earlier tag of any kind reaches |
| +(`git log --first-parent <tag> ^<base>…`). So v0.22.2 lists v0.22.1's |
| +change, which it shipped, and v0.3.0, cut from a commit unpublished |
| +v0.2.0 already held, lists what it actually shipped. A commit listed |
| +under two tags is the same change, joined once. |
| **Branch name**, from the first match in the message: `Squashed from (\S+) |
| into`, `Merge branch '([^']+)'`, `Merge pull request #\d+ from |
| @@ -142,10 +154,23 @@ into`, `Merge branch '([^']+)'`, `Merge pull request #\d+ from |
| **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; |
| +- a squash with a tombstone `refs/amadan/closed/<branch>` no newer than |
| + the squash: `<squash>^..<tombstone>`, non-merge commits only; |
| - otherwise the commit itself. |
| +Releases and their commits are joined oldest first, and any commit an |
| +earlier change already counted is dropped from a later one's own |
| +commits **(astra 2)**. Otherwise a branch stacked on one squashed before |
| +it (`4d13aec3` on `99084d7a`) re-counts the earlier branch's 17 commits, |
| +because squashing gave them new ids on main. |
| + |
| +**The line a change shows (run)** is its landing commit's subject, |
| +unless that only names the branch ("fix-x", "Merge branch 'x' into |
| +main"). Then it is the first informative line of the body (a GitHub PR |
| +title, an Amadan merge's description), and failing that the subject of |
| +the branch's first commit. Every option is a verbatim line from |
| +history. "Merge branch 'main' of <url>" is a sync merge, not a branch. |
| + |
| **Credit.** For each own commit, read the author and every |
| `Co-Authored-By` trailer. An identity resolves to a person, an agent, or |
| both: |
| @@ -175,9 +200,11 @@ 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 |
| +mapped to the nearest enclosing heading of level two to four (the levels |
| +the website anchors) in the new file, then kept only if today's corpus |
| +still anchors that heading **(astra 1)**. A line under the page's h1 |
| +links the page, unless a section of the same page is already linked. 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. |
| @@ -224,14 +251,21 @@ In `carlosframework/website`: |
| ## 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/committed_test.go` gates the committed file. It |
| + parses with no unknown fields; tags are unique and semver-ordered |
| + newest first; published, notes and notes URL agree; every `/docs` link |
| + (change, bullet, and markdown links in notes) resolves to a page and an |
| + anchored heading; `CheckPublic` passes; every `changelog-links.json` |
| + key matches a bullet whose branches agree. |
| +- **Completeness (astra 4).** In a checkout with tags, every tag up to |
| + the newest release in the file must be in it, and each release's |
| + changes must equal git's first-parent commits under the range rule. A |
| + tag newer than the file is a release not yet regenerated, and is only |
| + logged. Amadan CI clones at depth 1 without tags, so there the test |
| + skips and says why. It runs in any full checkout, including the one |
| + the release recipe regenerates from. Astra suggested committing an |
| + inventory for CI instead; an inventory the generator writes is not |
| + independent of the generator, so it was not added. |
| - `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 |
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-1.md b/docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-1.md| new file mode 100644 |
| index 000000000..3ddfbcd11 |
| --- /dev/null |
| +++ b/docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-1.md |
| @@ -0,0 +1,23 @@ |
| +Reviewed at `fb26eebc`; I edited no files. Closed-branch refs were absent, and network restrictions prevented fetching them or reading release bodies/discussion metadata. Those external claims remain unverified. |
| + |
| +1. **P1 — Docs-link derivation generates anchors the website never renders.** |
| + **Spec:** [lines 176–180](/home/paulca/amadan.net/carlos/platform-changelog/docs/superpowers/specs/2026-10-06-platform-changelog-design.md:176). |
| + **Evidence:** `git diff 4235e9fd^ 4235e9fd --unified=0 -- docs/site/cli.md` shows additions at lines 14–16, beneath `# The CLI`. The proposed rule produces `/docs/cli#the-cli`. `docsite.Load` includes that heading, but the website’s `markdown-it-anchor` configuration only anchors levels 2–4; rendering the heading produces `<h1>The CLI</h1>` without an ID. The platform gate accepts this link and the proposed website gate rejects it. |
| + **Fix:** Derive fragments only for heading levels the renderer anchors; use the page URL for introductory content. Add this actual commit as a regression case. |
| + |
| +2. **P2 — Squashed dependencies remain in a stacked branch’s “own commits.”** |
| + **Spec:** [lines 145–146](/home/paulca/amadan.net/carlos/platform-changelog/docs/superpowers/specs/2026-10-06-platform-changelog-design.md:145). |
| + **Evidence:** Squash `4d13aec3` names `feat/hostclaim-genselect` and records reviewed tip `d7621a29`. Running `git rev-list --count --no-merges 4d13aec3^..d7621a29` returns **22**. Seventeen are hostclaim-library commits already delivered by preceding squash `99084d7a`. Excluding their original tip, `47426cc1`, returns **5**. Squashing made the delivered work reachable under different commit IDs, so subtracting the mainline parent cannot remove it. |
| + **Fix:** Account for previously landed dependencies using their original branch tips or explicit provenance. Test a stacked branch whose dependency was squash-merged first; verify counts, credit weighting, and discussion references. |
| + |
| +3. **P1 — The committed-data gate permits public privacy violations.** |
| + **Spec:** [lines 227–233](/home/paulca/amadan.net/carlos/platform-changelog/docs/superpowers/specs/2026-10-06-platform-changelog-design.md:227). |
| + **Evidence:** The specified checks reject known identity addresses and agent-shaped addresses, but an ordinary address absent from `people.json` can remain in a discussion title or `notes.md`. Likewise, a Markdown link to the private `github.com/carlosframework/platform` repository passes: URL restrictions cover branch/discussion links, while the website check covers `/docs` links. These are concrete mutations the proposed gates would accept, despite the broader generator promise at lines 113–114. |
| + **Fix:** Apply shared public-output validation in both generation and the committed-file gate. Check all text fields for email addresses and all rendered Markdown/HTML link destinations for prohibited private repositories. Include negative fixtures for both cases. |
| + |
| +4. **P2 — Neither gate verifies release or change completeness.** |
| + **Spec:** [lines 227–234](/home/paulca/amadan.net/carlos/platform-changelog/docs/superpowers/specs/2026-10-06-platform-changelog-design.md:227), with website checks at lines 221–223. |
| + **Evidence:** Remove `v0.1.0` from otherwise valid JSON: ordering, uniqueness, links, and bullet mappings still pass—bullet mappings begin at `v0.19.0`. The website then checks section IDs only for the remaining releases. Similarly, an empty `changes` array for an older release passes. Both violate the fixed requirement to list every release and change. |
| + **Fix:** Validate release membership and per-release commit sets against an independent inventory derived from Git. If CI must remain offline without tags, commit that inventory and add omission tests; checking only entries present in the output cannot establish completeness. |
| + |
| +**Verdict: accept with fixes.** |
| \ No newline at end of file |