carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# 🤖 The platform changelog (carlosframework.com/platform/changelog)
2
3Status: rev 2, 2026-10-06. Rev 1 had Astra's review (accept with fixes,
4`CODEX-FINDINGS-CHANGELOG-1.md`); every finding is taken and marked
5**(astra N)**. The first real run over all 28 tags added three more,
6marked **(run)**. Asked for by Paul the same day: "a page which
7lists every change for every release, links to docs, links to the Amadan
8branch/discussion and shows who did the work".
9
10## Rulings (Paul, 2026-10-06; not to reopen)
11
121. **A change is a merged branch.** Each release lists every first-parent
13 commit that landed on `main` since the previous tag, which since v0.20.0
14 is one merged branch each. Groundwork, docs and fixes all count. The
15 release's published notes sit on top as the summary.
162. **Every release, back to v0.1.0, with a sliding link depth.** From the
17 Amadan era on, a change links to its Amadan branch and discussions.
18 Before that it links to its commit on Amadan (public) and credits the git
19 authors. The GitHub mirror is private, so no GitHub PR links.
203. **Change lines are records, not copy.** A change shows its branch name
21 and squash subject verbatim, styled as data, under a visible 🤖. Only
22 the page's own framing and the already-reviewed release notes are prose.
23 The framing goes through copy review.
244. **The join runs in the platform repo.** A generator writes a committed
25 `docs/site/changelog.json`; the website vendors it with the docs corpus
26 and renders it at build time. No network at website build.
27
28## What exists (verified 2026-10-06)
29
30- 28 tags, v0.1.0 (2026-08-02) to v0.24.2. v0.2.0 and v0.22.1 were tagged
31 but never published on `carlosframework/releases`.
32- Merge shapes on `main`'s first-parent line: amadan squashes ("Squashed
33 from X into main." in the body), amadan merges ("Merge branch 'X' into
34 main"), GitHub merges ("Merge pull request #N from carlosframework/X"),
35 and before those, direct commits. All 55 first-parent commits since
36 v0.20.0 name their branch.
37- Amadan keeps a tombstone ref for each merged branch at
38 `refs/amadan/closed/<branch>` (151 today), so a squashed branch's own
39 commits stay reachable. Its page is `https://amadan.net/carlos/platform/-/branch/<url-escaped name>`;
40 a commit is `https://amadan.net/carlos/platform/commit/<sha>`; a
41 discussion is `https://amadan.net/carlos/platform/-/discussion/<id>`.
42- `amadan discuss show carlos/platform <id> -json` and `amadan discuss
43 list carlos/platform -branch <b> -state all -json` read discussions. The
44 unfiltered list caps at 50. There is no read for a branch's description.
45- Authors: people (`Paul Campbell`, `Bill Horsman`, `James Adam`, `Tony
46 Wood`, under several emails) and agents whose email carries their
47 operator (`claude~paul@keymail.dev`, `codex~paul@keymail.dev`,
48 `claude-code~bill@keymail.dev`). Older person-authored commits carry
49 `Co-Authored-By: Claude …` trailers.
50- Release notes changed shape. v0.19.0 on: a `carlos vX (platform …)` line,
51 `- **Bold lead.** …` bullets, an operator section (`**For operators**`,
52 `**Before you roll**`, `**Before you roll a console to this release**`),
53 and a closing "Verify a download…" line. Before v0.19.0: free markdown
54 with `##` headings, tables, fences, an `## Install` section and a
55 "🤖 Generated with Claude Code" line.
56- carlosframework.com is `carlosframework/website` (Eleventy, plain CSS, no
57 browser JS but the AI disclaimer). `hack/sync-docs.mjs` vendors
58 `docs/site/` from a platform checkout; `hack/check-docs.mjs` checks
59 the build's internal links. Its rule: every AI-written page carries a
60 visible 🤖, and one on the top heading covers the page.
61
62## The data: `docs/site/changelog.json`
63
64```json
65{
66 "//": "generated by go run ./cmd/changelog; do not edit",
67 "repo": "https://amadan.net/carlos/platform",
68 "releases": [
69 {
70 "tag": "v0.24.2",
71 "date": "2026-10-06",
72 "sha": "7f403a77…",
73 "published": true,
74 "notes_url": "https://github.com/carlosframework/releases/releases/tag/v0.24.2",
75 "notes": {
76 "form": "bullets",
77 "sections": [
78 {"title": "", "bullets": [
79 {"md": "**CI can deploy through the console.** …",
80 "branches": ["design-deploy-credential"],
81 "docs": ["/docs/cli#carlos-services"]}
82 ]},
83 {"title": "For operators", "bullets": [ … ]}
84 ]
85 },
86 "changes": [
87 {
88 "sha": "d1dab9a9…",
89 "date": "2026-10-06",
90 "subject": "deploy-credential: a rung-scoped deploy credential so CI promotes through the console (#229)",
91 "branch": "design-deploy-credential",
92 "branch_url": "https://amadan.net/carlos/platform/-/branch/design-deploy-credential",
93 "commits": 14,
94 "discussions": [{"id": 229, "title": "CI cannot promote …", "state": "closed"}],
95 "docs": ["/docs/cli#carlos-services"],
96 "credit": [{"person": "Paul Campbell", "agents": ["claude"]}]
97 }
98 ]
99 }
100 ]
101}
102```
103
104- `notes.form` is `"bullets"` for the v0.19.0-on shape and `"markdown"`
105 otherwise. A markdown body is kept whole as `notes.md`, with headings
106 demoted two levels (the release is the page's `h2`) and the
107 boilerplate removed: the `carlos vX (platform …)` line, the "Verify a
108 download" line, an `## Install` section, and the "Generated with Claude
109 Code" line. Unpublished tags have `"notes": null`, and so does a
110 release published with an empty body (v0.18.0). The gates accept
111 exactly the two forms the page renders, so a bad `form` cannot drop a
112 release's notes silently.
113- Bullet sections keep the notes' own operator heading text, minus the
114 bold markers.
115- `subject` is the commit's subject line. `branch_url` is set only when
116 the branch has an Amadan record (a tombstone ref, a live head, or a
117 squash/merge made on Amadan). A GitHub-era branch keeps its `branch`
118 name with no link.
119- No email address and no link into a private repo ever appears in the
120 file **(astra 3)**. Commit subjects and discussion titles are quoted
121 with any address replaced by `[address]` and any private-repo URL by
122 `[private repo]`. Discussion #147's title names a customer's address,
123 and a 2026-08-03 sync merge names the private mirror. Anything left,
124 in any field, fails the run: `CheckPublic`, shared by the generator
125 and the gate.
126
127## The generator: `cmd/changelog`, logic in `internal/changelog`
128
129`go run ./cmd/changelog [-out docs/site/changelog.json]`
130
131Run from a full checkout after `git fetch origin --tags
132'+refs/amadan/closed/*:refs/amadan/closed/*'`. It needs `gh` (release
133bodies) and `amadan` (discussions) signed in. It writes the file and
134prints one summary line per release.
135
136**Inputs, each behind an interface** so tests run on fakes and a scratch
137git repo:
138
139- `Git`: tags with dates, first-parent commits in a range, a commit's
140 message, parents and changed `docs/site/*.md` hunks, and the commits
141 reachable from a tombstone but not from `main`.
142- `Releases`: release body by tag, or not-found.
143- `Discussions`: one by id, and the list for a branch.
144
145**Ranges (run).** Tags sort by semver. A published release covers the
146first-parent commits no earlier *published* release reaches; an
147unpublished tag covers those no earlier tag of any kind reaches
148(`git log --first-parent <tag> ^<base>…`). So v0.22.2 lists v0.22.1's
149change, which it shipped, and v0.3.0, cut from a commit unpublished
150v0.2.0 already held, lists what it actually shipped. A commit listed
151under two tags is the same change, joined once.
152
153**Branch name**, from the first match in the message: `Squashed from (\S+)
154into`, `Merge branch '([^']+)'`, `Merge pull request #\d+ from
155[^/\s]+/(\S+)`. None: a direct commit, `branch` empty.
156
157**The branch's own commits**, for credit and discussion references:
158
159- a two-parent merge: `<first parent>..<second parent>`;
160- a squash with a tombstone `refs/amadan/closed/<branch>` no newer than
161 the squash: `<squash>^..<tombstone>`, non-merge commits only;
162- otherwise the commit itself.
163
164Releases and their commits are joined oldest first, and any commit an
165earlier change already counted is dropped from a later one's own
166commits **(astra 2)**. Otherwise a branch stacked on one squashed before
167it (`4d13aec3` on `99084d7a`) re-counts the earlier branch's 17 commits,
168because squashing gave them new ids on main.
169
170**The line a change shows (run)** is its landing commit's subject,
171unless that only names the branch ("fix-x", "Merge branch 'x' into
172main"). Then it is the first informative line of the body (a GitHub PR
173title, an Amadan merge's description), and failing that the subject of
174the branch's first commit. Every option is a verbatim line from
175history. "Merge branch 'main' of <url>" is a sync merge, not a branch.
176
177**Credit.** For each own commit, read the author and every
178`Co-Authored-By` trailer. An identity resolves to a person, an agent, or
179both:
180
181- `<agent>~<keymail>`: agent `<agent>` (`claude-code` reads `claude`),
182 person = `people[<keymail>]`.
183- `noreply@anthropic.com` or a name starting `Claude`: agent `claude`, no
184 person. `codex` names: agent `codex`.
185- Anything else: person = `people[<email>]`.
186- `noreply@github.com` and `ledger@amadan` are skipped.
187
188`docs/site/people.json` maps every email and keymail to a display name.
189An identity with no entry fails the run and names the address, so a new
190contributor is a one-line decision, not a leaked email. Each identity
191keeps its own pairing: an agent account credits its operator with that
192agent, and a person co-author is credited alone. An agent with no
193person of its own (a `Co-Authored-By: Claude` trailer) attaches to the
194commit's author, or failing that the first co-author who is a person,
195never to every person on the commit (Astra's code review: `da866a92`
196had credited Tony Wood "with claude"). Credit groups by person, ordered
197by commit count, agents listed per person; an agent with no person
198anywhere is its own entry.
199
200**Discussions.** References come from the squash message and the own
201commits' messages: `#(\d+)`, `discussion (\d+)`, `amadan #(\d+)`. A
202number counts only when `discuss show` finds it and it was created before
203the commit, which rejects GitHub-era issue and PR numbers. Plus every
204discussion `discuss list -branch <branch>` returns. Deduped, sorted by id.
205Each lookup is cached for the run.
206
207**Docs links.** For a change: each `docs/site/<page>.md` the commit (the
208squash, or the merge's diff against its first parent) adds lines to,
209mapped to the nearest enclosing heading of level two to four (the levels
210the website anchors) in the new file, then kept only if today's corpus
211still anchors that heading **(astra 1)**. A line under the page's h1
212links the page, unless a section of the same page is already linked. A
213page that no longer exists drops out. For a
214release-note bullet: each inline code span starting `carlos <verb>` whose
215longest matching CLI heading (`carlos <verb>`, `carlos <verb> <sub>`) exists
216on the CLI page. Deduped, at most five per change.
217
218**Bullet to branch links.** `docs/site/changelog-links.json`, hand-kept:
219
220```json
221{"v0.24.2": {"CI can deploy through the console.": ["design-deploy-credential"]}}
222```
223
224keyed by the bullet's bold lead. The generator fails on a key that
225matches no bullet in that release, or a branch that is not one of that
226release's changes. Bullets without an entry have no branch links. The
227backfill covers v0.19.0 on; older notes are markdown and get none.
228
229## The page: `/platform/changelog/`
230
231In `carlosframework/website`:
232
233- `hack/sync-docs.mjs` vendors `docs/site/changelog.json` as
234 `src/_data/docschangelog.json` with the usual `"//"` notice (the sync
235 already owns `src/_data/docs*.json`).
236- `src/changelog.njk` with `permalink: /platform/changelog/index.html`,
237 in the platform page's world: its masthead, the platform blue accent,
238 `site.css`, a page-local `<style>` only where `site.css` has nothing.
239- The top heading carries the visible 🤖, which covers the page. A short
240 intro (copy-reviewed) says what a change is, that lines are verbatim
241 records, and how far back links go.
242- One `<section id="v0-24-2">` per release, newest first: the tag as `h2`
243 with a self link, date, a link to the published notes (or "not
244 published"), then the notes (bullets with their branch and docs links;
245 the operator section in a `<details>`; markdown notes rendered by the
246 site's markdown-it), then "Changes (N)" as a list. Each change: branch
247 name (linked when it has a record) or short SHA for a direct commit,
248 the subject in mono, discussions as `#229` links titled with the
249 discussion title, docs links, the credit ("Paul Campbell with claude"),
250 and the short SHA linked to its commit.
251- A release with more than 25 changes shows the first 25 and the rest in
252 a `<details>`. No script.
253- `/platform/` gets one link to the changelog (copy-reviewed).
254- `hack/check-docs.mjs` also checks that the page built, that every
255 release has its section id, and that every `/docs` link on it
256 resolves, using the code it already has for docs pages.
257
258## Gates and upkeep
259
260- `internal/changelog/committed_test.go` gates the committed file. It
261 parses with no unknown fields; tags are unique and semver-ordered
262 newest first; published, notes and notes URL agree; every `/docs` link
263 (change, bullet, and markdown links in notes) resolves to a page and an
264 anchored heading; `CheckPublic` passes; every `changelog-links.json`
265 key matches a bullet whose branches agree.
266- **Completeness (astra 4).** In a checkout with tags, every tag up to
267 the newest release in the file must be in it, and each release's
268 changes must equal git's first-parent commits under the range rule. A
269 tag newer than the file is a release not yet regenerated, and is only
270 logged. Amadan CI clones at depth 1 without tags, so there the test
271 skips and says why. It runs in any full checkout, including the one
272 the release recipe regenerates from. Astra suggested committing an
273 inventory for CI instead; an inventory the generator writes is not
274 independent of the generator, so it was not added.
275- `internal/changelog` has unit tests for each parser and resolver on
276 fakes, and one test on a scratch git repo that builds tags, a squash with
277 a tombstone, a two-parent merge and a direct commit, and checks the whole
278 output.
279- The README's release recipe gains a step after Publish: regenerate the
280 changelog, add the new release's bullet links, commit on a branch, merge,
281 then re-sync and ship the website. (Copy-reviewed.)
282
283## Asks of Amadan (filed, not worked around)
284
2851. A read for a branch's metadata, including merged ones: description,
286 landed SHA, who pushed. Until then a change's line is its squash
287 subject.
2882. Paging on `discuss list` (it caps at 50). The generator does not need
289 it: it reads by id and by branch.
290
291## Out of scope
292
293- An RSS or Atom feed. It's easy to add from the same data later.
294- Per-change prose summaries (ruling 3).
295- Changing how release notes are written.
296- Rolling or deploying anything beyond the website's own ship and
297 promote, which waits for the copy review.
298