docs(contributing-with-ai): add 8 rules surfaced by recent feature PR

Pulled from a retrospective on a feature PR that exercised every rule
already in the doc, and surfaced eight gaps where the doc was silent.

- Protocol-introducing changes (new section): require an explicit
  cross-client compatibility section, on-relay wire-format verification
  via nak, and a NIP spec citation when claiming "NIP-X allows this".
  These three together prevent the "syncs across devices" handwave
  from masking real interop questions.
- Release-minified build subsection in "Build and install both
  flavours": call out the existing benchmark build type
  (com.vitorpamplona.amethyst.benchmark, profileable=true, R8-active)
  as the cheap rig for catching ViewModel-factory / expect-actual /
  serialization R8-stripping bugs before the reviewer does.
- Strip diagnostic Log.d before commit: explicit policy. Lambda Log
  is required for committed logs; temporary debug Log.d gets removed.
- Regression-test-plan worked example: shows the required
  ### Feature test plan / ### Regression test plan structure with a
  real-shaped example, so contributors don't ship the wrong subheadings.
- Multi-device sync touch-point: added to the regression sweep
  checklist for features that publish per-account state to relays.
- R8-minified-build touch-point: added to the same checklist for
  reflection-heavy code paths.
- "When on-device QA finds bugs in your own diff" subsection in
  "Code review pass": promote the pattern of landing manual-QA
  fixes as a separate commit with root-cause prose, not folding
  silently into the feature commit.
- Working-notes convention in "Everything else": clarify
  docs/plans/ is tracked, docs/brainstorms/ + docs/superpowers/
  are gitignored. Saves AI tools from trying to commit scratch work.
This commit is contained in:
davotoula
2026-05-13 11:05:32 +02:00
parent 1d503dc6ac
commit 286344c254
+113
View File
@@ -11,6 +11,7 @@ If you are not using an AI assistant, you can stop reading here.
- [Research before code](#research-before-code) - [Research before code](#research-before-code)
- [Build and install both flavours](#build-and-install-both-flavours) - [Build and install both flavours](#build-and-install-both-flavours)
- [Protocol-introducing changes](#protocol-introducing-changes)
- [Performance and resource hygiene](#performance-and-resource-hygiene) - [Performance and resource hygiene](#performance-and-resource-hygiene)
- [Automated tests for new logic](#automated-tests-for-new-logic) - [Automated tests for new logic](#automated-tests-for-new-logic)
- [Regression test plan](#regression-test-plan) - [Regression test plan](#regression-test-plan)
@@ -68,6 +69,50 @@ If a change is conclusively flavour-irrelevant (a pure `quartz/`
protocol fix, a docs change, a translation), one flavour is enough — protocol fix, a docs change, a translation), one flavour is enough —
say which and why in the PR description. say which and why in the PR description.
### Test the release-minified build, not just `*-debug`
Reflection-touched code paths — `ViewModelProvider.Factory`,
`expect`/`actual` boundaries, serialization, custom Compose runtime
machinery — silently break under R8 / ProGuard while compiling fine
in `*-debug` builds. The cheapest way to catch this on this codebase
is the `benchmark` build type:
```bash
./gradlew :amethyst:installPlayBenchmark
```
That installs `com.vitorpamplona.amethyst.benchmark` ("Amy Benchmark"
on the home screen) — R8-minified, release-flavoured, `profileable =
true`, side-by-side with your normal Amethyst install via the
`.benchmark` applicationId suffix. Sign in to a test npub and exercise
the new flow there before opening the PR. A feature that crashes or
silently no-ops in the benchmark build but works in `*-debug` is a
missing keep rule — file it before reviewers find it.
## Protocol-introducing changes
If your PR publishes a new event kind, a new tag form, a new marker,
or a new interpretation of an existing NIP clause, the diff must also
include:
- **Cross-client compatibility section in the PR description.** State
explicitly: which earlier Amethyst versions honour the new payload
(forward/backward compat), whether other major clients (Damus,
Primal, Iris, Coracle, etc.) implement the same NIP clause today,
and what user-visible behaviour differs for users still on older
builds or other clients. Don't paper over this with "syncs across
devices" — say which devices, which clients, and which versions.
- **Wire-format verification.** Before opening the PR, fetch the
published event from a relay and confirm its tags, content, and
encryption envelope match what you intended. `nak req -i <event-id>
wss://<relay>` is the standard tool. Paste the relevant tag line
(redact private content) into the PR description. This catches bugs
that don't show locally — wrong markers, missing relay hints,
malformed private-tag JSON.
- **NIP citation.** If you claim "NIP-X allows this", quote the exact
spec line in the PR. A reviewer should not have to re-derive the
authority for your tag form.
## Performance and resource hygiene ## Performance and resource hygiene
Code that looks fine in review can wreck the app at runtime. The Code that looks fine in review can wreck the app at runtime. The
@@ -141,6 +186,15 @@ device. PRs that introduce any of them will be sent back.
body only runs when the log level is enabled. Plain body only runs when the log level is enabled. Plain
`Log.d("msg $x")` allocates the formatted string on every call, `Log.d("msg $x")` allocates the formatted string on every call,
including in feed and scroll hot paths. including in feed and scroll hot paths.
- **Strip diagnostic `Log.d` calls before commit.** Logs added
during on-device debugging — even lambda-form ones — must be
removed from the production diff. They survive R8 stripping only
in debug builds, so committed `Log.d` doesn't directly hurt
release performance, but it bloats the diff, scatters noise across
logcat for the next developer, and silently grows over time. If a
log line is genuinely load-bearing for future
incident-response, promote it to `Log.i`/`w` with explicit
justification in the commit message.
Relevant skills under `.claude/skills/`: `account-state`, Relevant skills under `.claude/skills/`: `account-state`,
`relay-client`, `kotlin-coroutines`, `kotlin-multiplatform`, `relay-client`, `kotlin-coroutines`, `kotlin-multiplatform`,
@@ -199,6 +253,39 @@ parts under these exact subheadings:
Don't add a new top-level section to the PR description — put both Don't add a new top-level section to the PR description — put both
subheadings inside the existing **Test plan** section. subheadings inside the existing **Test plan** section.
Worked example of the required structure (real shape from a recent
feature PR; substitute your own touch points and verifications):
````markdown
## Test plan
### Feature test plan
- Long-press a reply note → quick-action sheet → "Mute thread" →
thread disappears from Home immediately.
- Force-stop the app, relaunch → muted thread still hidden (relay
round-trip verified).
- Settings → Security & Filters → Muted threads → tap "Unmute" →
thread reappears in Home.
### Regression test plan
Touch points and verification:
- Existing user-mute — could regress through the shared
`Account.isAcceptable` chokepoint — verified: muted a different
user, posts hidden as before.
- Hidden words — same chokepoint — verified: added a word, posts
filtered; removed it, posts back.
- Multi-account switch — could leak mute list across accounts —
verified: switched between two npubs, each saw only its own mutes.
- R8-minified build — could fail on new ViewModel factory — verified
on `installPlayBenchmark`.
- Both flavours — built and installed `installPlayDebug`; F-Droid
build is flavour-irrelevant for this change (no Google-proprietary
touch points), built only.
````
For the regression test plan, list: For the regression test plan, list:
1. **Touch points** — screens, flows, ViewModels, shared state, or 1. **Touch points** — screens, flows, ViewModels, shared state, or
@@ -222,12 +309,20 @@ Worked example, for "add a new field to `Account`":
Common touch-point categories worth scanning every PR for: Common touch-point categories worth scanning every PR for:
- Account / login / logout / multi-account switch. - Account / login / logout / multi-account switch.
- **Multi-device sync when the feature publishes per-account state to
relays.** Sign in to the same npub on a second device and verify
the new state propagates and is honoured there. State explicitly
which Amethyst versions both sides need to be running for the sync
to work (current build only, or older builds too).
- Both flavours (Play and F-Droid). - Both flavours (Play and F-Droid).
- Feed types: home, profile, hashtag, bookmarks, notifications, - Feed types: home, profile, hashtag, bookmarks, notifications,
DMs, communities. DMs, communities.
- Orientation changes. - Orientation changes.
- Cold start vs warm start. - Cold start vs warm start.
- Background → foreground transitions. - Background → foreground transitions.
- Release-minified build (`installPlayBenchmark`) for any code path
that touches reflection — ViewModel factories, expect/actual,
custom serialization.
If a touch point can't reasonably be verified (it would require a If a touch point can't reasonably be verified (it would require a
relay matrix you don't have, or a device combination you can't relay matrix you don't have, or a device combination you can't
@@ -258,6 +353,24 @@ If the review flags issues, either address them or document in the
PR description why you accept the risk. Don't silently discard PR description why you accept the risk. Don't silently discard
review output. review output.
### When on-device QA finds bugs in your own diff
On-device QA frequently surfaces defects after the initial
implementation reads as "done". Don't fold those fixes silently
into the feature commit and force-push — that erases the signal that
the defect existed and what it was. Instead:
- Land each fix as its own commit, or a final squashed
`"Manual testing fixes"` commit at the tip of the branch.
- The commit message body names the root cause, not just the
symptom. Answer the question "why was this missing from the initial
diff?" — undocumented event-shape variant, collision with a
recently-merged upstream change, Compose recomposition assumption,
R8 keep-rule gap, lifecycle race.
- A reviewer reading your branch sees: feature → code-review cleanup
→ on-device manual-QA fixes. That's a healthy development arc, not
a liability.
## Don't touch without an issue first ## Don't touch without an issue first
Open an issue and get explicit maintainer alignment before opening a Open an issue and get explicit maintainer alignment before opening a