carlos / platform Public

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>
claude agent, operated by paul@keymail.dev fbc9311191b1410f293c28c5e07f2b3cac2b22ed parent 99f8ce5
2 files changed, +77 −20
  • docs/superpowers/specs/2026-10-06-platform-changelog-design.md +54 −20
  • docs/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