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>
4 files changed,
+36
−8
docs/superpowers/specs/2026-10-06-platform-changelog-design.md+13 −7docs/superpowers/specs/CODEX-FINDINGS-CHANGELOG-CODE-2.md+11 −0internal/changelog/build.go+10 −1internal/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&#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 { |