carlos / platform Public

changelog: finish Astra's finding 1 (round 2), record the review in the spec

CheckPublic also reads strings the way a browser shows them: markdown
emphasis stripped and dot segments collapsed, so a split address or
github.com/carlosframework/./platform fails. The website gate checks
the rendered page itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 08467448bb233fb16e4d1a052fb41b12f89e06f1 parent d10798d
4 files changed, +36 −8
  • docs/superpowers/specs/2026-10-06-platform-changelog-design.md +13 −7
  • docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-CODE-2.md +11 −0
  • internal/changelog/build.go +10 −1
  • internal/changelog/build_test.go +2 −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 46dee6fb3..874404421 100644
--- a/docs/superpowers/specs/2026-10-06-platform-changelog-design.md
+++ b/docs/superpowers/specs/2026-10-06-platform-changelog-design.md
@@ -106,7 +106,10 @@ branch/discussion and shows who did the work".
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`.
+ Code" line. Unpublished tags have `"notes": null`, and so does a
+ release published with an empty body (v0.18.0). The gates accept
+ exactly the two forms the page renders, so a bad `form` cannot drop a
+ release's notes silently.
- 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
@@ -184,12 +187,15 @@ both:
`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.
+contributor is a one-line decision, not a leaked email. Each identity
+keeps its own pairing: an agent account credits its operator with that
+agent, and a person co-author is credited alone. An agent with no
+person of its own (a `Co-Authored-By: Claude` trailer) attaches to the
+commit's author, or failing that the first co-author who is a person,
+never to every person on the commit (Astra's code review: `da866a92`
+had credited Tony Wood "with claude"). 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
diff --git a/docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-CODE-2.md b/docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-CODE-2.md
new file mode 100644
index 000000000..9fd3291cd
--- /dev/null
+++ b/docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-CODE-2.md
@@ -0,0 +1,11 @@
+| Previous finding | Disposition | Evidence |
+|---|---|---|
+| **1. P1 — Encoded addresses and private links bypass validation** | **Not fully fixed** | Original entity and percent-encoding examples now fail validation. However, `[source](https://github.com/carlosframework/./platform)` still passes the rendered-page gate; the browser resolves it to the private platform repository. Neither [Go decoding](/home/paulca/amadan.net/carlos/platform-changelog/internal/changelog/build.go:616) nor the [website check](/home/paulca/github.com/carlosframework/website-changelog/hack/check-docs.mjs:142) normalizes URL paths. Likewise, `ops**@**example.org` renders a visible address and passes. **Remaining fix:** validate rendered text and parsed, normalized link destinations. |
+| **2. P1 — Invalid notes form silently removes notes** | **Fixed** | [Go validation](/home/paulca/amadan.net/carlos/platform-changelog/internal/changelog/committed_test.go:53) requires a recognized form and matching payload. The [website gate](/home/paulca/github.com/carlosframework/website-changelog/hack/check-docs.mjs:134) now rejects the original v0.24.2 `"bullet"` mutation; confirmed with in-memory rendering. |
+| **3. P2 — Every agent assigned to every person** | **Fixed** | [Tally](/home/paulca/amadan.net/carlos/platform-changelog/internal/changelog/credit.go:126) preserves identity pairings. Landing `da866a92` still counts 24 own commits, but its [generated credit](/home/paulca/amadan.net/carlos/platform-changelog/docs/site/changelog.json:1115) now correctly lists Paul with Claude and Tony alone, consistent with `9fe9ec28`. |
+
+No new P1 or P2 findings. The remaining privacy failures belong to finding 1.
+
+Reviewed platform `d10798d0` and website `973f407`. All 28 release ranges and 363 unique changes’ own-commit counts match Git. Existing website checks, vendored-data comparison, and in-memory rendering checks ran. Go tests could not start because the sandbox blocks their work directory; full `npm run check` requires writes. No files edited.
+
+**accept with fixes**
\ No newline at end of file
diff --git a/internal/changelog/build.go b/internal/changelog/build.go
index d6351e696..80eaa6025 100644
--- a/internal/changelog/build.go
+++ b/internal/changelog/build.go
@@ -628,9 +628,18 @@ func decodedForms(s string) []string {
}
return m
})
- return []string{s, ent, md, pct, all}
+ // What a browser makes of markdown emphasis and dot segments:
+ // "**ops@example**.org" reads as an address, and
+ // "carlosframework/./platform" resolves to the private repo.
+ plain := dotSegmentRe.ReplaceAllString(emphasisRe.ReplaceAllString(all, ""), "/")
+ return []string{s, ent, md, pct, all, plain}
}
+var (
+ emphasisRe = regexp.MustCompile("[*_`~]+")
+ dotSegmentRe = regexp.MustCompile(`/(\./)+`)
+)
+
func checkPublicText(s string, people People) error {
for addr := range people {
if strings.Contains(addr, "@") && strings.Contains(s, strings.ToLower(addr)) {
diff --git a/internal/changelog/build_test.go b/internal/changelog/build_test.go
index 05d333c2d..1af759d28 100644
--- a/internal/changelog/build_test.go
+++ b/internal/changelog/build_test.go
@@ -243,6 +243,8 @@ func TestCheckPublic(t *testing.T) {
"[source](https://github.com/carlosframework/plat%66orm)",
"write to ops\\@example.org",
"ops&amp;#64;example.org",
+ "see **ops@example**.org now",
+ "[source](https://github.com/carlosframework/./platform)",
} {
bad := &Changelog{Releases: []Release{{Tag: "v1.0.0", Changes: []Change{{Subject: s}}}}}
if err := CheckPublic(bad, testPeople); err == nil {