fd02b47d9a
Two additional gates on top of the AI-companion guide:
- Automated tests for new logic — quartz/ and commons/ get unit
tests; bug fixes get a regression test; UI-only stays exempt
from automated UI tests but keeps the manual on-device test
plan + screenshots. Pointer into CONTRIBUTING.md § Interop tests.
- Code review pass before opening the PR — run a second-agent
review with a different model or skill (/simplify, /kotlin-review,
/security-review, /code-review), then re-run tests and manual
test plan to catch fix-introduced regressions.
CONTRIBUTING.md pointer paragraph updated to enumerate the two new
gates.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
288 lines
12 KiB
Markdown
288 lines
12 KiB
Markdown
# Contributing to Amethyst with AI Assistance
|
|
|
|
This document is a companion to [`CONTRIBUTING.md`](CONTRIBUTING.md).
|
|
Everything in `CONTRIBUTING.md` applies to every contribution. This doc
|
|
adds gates specific to pull requests whose diff was substantially
|
|
authored by an AI coding assistant (Claude Code, Copilot, Cursor,
|
|
Codex, etc.). Where this doc and `CONTRIBUTING.md` differ, the
|
|
stricter rule wins.
|
|
|
|
If you are not using an AI assistant, you can stop reading here.
|
|
|
|
- [Research before code](#research-before-code)
|
|
- [Build and install both flavours](#build-and-install-both-flavours)
|
|
- [Performance and resource hygiene](#performance-and-resource-hygiene)
|
|
- [Automated tests for new logic](#automated-tests-for-new-logic)
|
|
- [Regression test plan](#regression-test-plan)
|
|
- [Code review pass before opening the PR](#code-review-pass-before-opening-the-pr)
|
|
- [Don't touch without an issue first](#dont-touch-without-an-issue-first)
|
|
- [Everything else](#everything-else)
|
|
|
|
---
|
|
|
|
## Research before code
|
|
|
|
AI agents are good at writing plausible-looking code for issues that
|
|
no longer make sense. Before you (or your assistant) write a line of
|
|
code, confirm the issue still wants to be implemented.
|
|
|
|
- **Issue still valid.** The issue is open, not superseded by a merged
|
|
PR, not blocked by a NIP change, and not declared out of scope. Old
|
|
bountied issues fail these checks routinely.
|
|
- **Post a research summary on the issue first.** A one-paragraph
|
|
comment stating your read of the problem, the approach you intend
|
|
to take, and the modules you expect to touch. Give maintainers a
|
|
chance to flag it as stale before you invest in a diff.
|
|
- **No duplicate PR.** Search open and recently-closed PRs for the
|
|
same feature. If a prior attempt exists, link to it and explain
|
|
what you do differently.
|
|
- **Fits Amethyst's nature.** The feature must work in a decentralised
|
|
client: no central server, no maintainer-controlled state, no
|
|
required third-party account. If the proposal assumes any of these,
|
|
the feature doesn't belong in Amethyst, regardless of the bounty.
|
|
- **NIPs still current.** If the issue references a specific NIP,
|
|
check it hasn't been deprecated or superseded.
|
|
|
|
## Build and install both flavours
|
|
|
|
Any change that could differ between flavours — UI, services,
|
|
dependencies, `AndroidManifest.xml`, ProGuard rules — must build and
|
|
install on both Play and F-Droid:
|
|
|
|
```bash
|
|
./gradlew installPlayDebug
|
|
./gradlew installFdroidDebug
|
|
```
|
|
|
|
Paste the `BUILD SUCCESSFUL` tail of both into the PR description.
|
|
|
|
**Why both.** F-Droid drops Google-proprietary dependencies (Play
|
|
Services, Firebase, Cast SDK, etc.). Code that compiles only on Play
|
|
is rejected. The canonical recent example is the Chromecast feature:
|
|
the Google Cast SDK is a Play-only dependency, so F-Droid required a
|
|
separate stub implementation under `amethyst/src/fdroid/`. Agents
|
|
routinely add Play-only imports without realising the F-Droid build
|
|
breaks.
|
|
|
|
If a change is conclusively flavour-irrelevant (a pure `quartz/`
|
|
protocol fix, a docs change, a translation), one flavour is enough —
|
|
say which and why in the PR description.
|
|
|
|
## Performance and resource hygiene
|
|
|
|
Code that looks fine in review can wreck the app at runtime. The
|
|
following are common AI-agent footguns in this codebase. None of them
|
|
trip CI — they only surface in careful manual review or on a real
|
|
device. PRs that introduce any of them will be sent back.
|
|
|
|
### UI thread and recomposition
|
|
|
|
- **No main-thread blocking.** Network, JSON parsing, regex, crypto,
|
|
file I/O, and DB queries belong on `Dispatchers.IO` or
|
|
`Dispatchers.Default`. Never `runBlocking { ... }` from a Composable,
|
|
click handler, or `LaunchedEffect`.
|
|
- **Hoist work out of `@Composable` bodies and `LazyColumn` item
|
|
content.** Parsing, list filtering, building maps, allocating data
|
|
classes all belong in `remember`, `derivedStateOf`, or the
|
|
ViewModel, not in the render path. New lambdas allocated per render
|
|
also defeat `@Stable` and cause unnecessary recomposition of
|
|
children.
|
|
- **Use `collectAsStateWithLifecycle()`** for Flow → Compose, not
|
|
`collectAsState()`, so collection pauses when the screen is
|
|
off-screen.
|
|
|
|
### Coroutines and scoping
|
|
|
|
- **No `GlobalScope.launch` and no ad-hoc `CoroutineScope(Job())`.**
|
|
Use `viewModelScope`, a lifecycle scope, or a passed-in
|
|
`CoroutineScope`, so cancellation propagates on logout, navigation,
|
|
or process death.
|
|
- **Don't put suspend work in `init {}`** of ViewModels. It runs on
|
|
whatever thread constructed the VM and can't be cancelled. Use a
|
|
`MutableStateFlow` + `viewModelScope.launch` pattern.
|
|
|
|
### Memory and caching
|
|
|
|
- **Don't build a parallel cache of Notes, Users, or Events.**
|
|
Amethyst stores them once in `LocalCache` (backed by `LargeCache`),
|
|
keyed by id or pubkey, mutable in place. A new
|
|
`mutableMapOf<HexKey, Note>()` in your feature doubles the working
|
|
set and gets stale.
|
|
- **Don't roll your own image cache.** Coil is wired up with
|
|
size-aware loaders. Decoding a full-resolution image yourself will
|
|
OOM mid-scroll.
|
|
- **Bound your collections.** Unbounded `mutableMapOf` or
|
|
`mutableListOf` that accrue per-event entries are memory leaks. If
|
|
you mean "the last N", use a size-bounded structure.
|
|
|
|
### Relay traffic and mobile network
|
|
|
|
- **Use the existing subscription layer.**
|
|
`ComposeSubscriptionManager`, `Subscribable`, and the filter
|
|
assemblers under `commons/.../relayClient/` are lifecycle-aware,
|
|
deduped, and EOSE-closed. Don't open ad-hoc WebSockets and don't
|
|
issue raw `REQ` filters from a Composable.
|
|
- **Don't re-fetch what's already in `LocalCache`.** Check the cache
|
|
first; only subscribe for what's missing.
|
|
- **Respect data-saver and connectivity context.** Auto-fetching
|
|
full-resolution video on cellular is a regression even if the code
|
|
technically works.
|
|
|
|
### KMP source-set discipline
|
|
|
|
- **Android-only imports don't belong in `commons/commonMain` or
|
|
`quartz/commonMain`.** Use `expect`/`actual` for platform-specific
|
|
bits, or move the Android-specific code to `androidMain`.
|
|
|
|
### Logging
|
|
|
|
- **Use the Quartz lambda Log.**
|
|
`com.vitorpamplona.quartz.utils.Log.d { "msg $x" }` — the lambda
|
|
body only runs when the log level is enabled. Plain
|
|
`Log.d("msg $x")` allocates the formatted string on every call,
|
|
including in feed and scroll hot paths.
|
|
|
|
Relevant skills under `.claude/skills/`: `account-state`,
|
|
`relay-client`, `kotlin-coroutines`, `kotlin-multiplatform`,
|
|
`find-non-lambda-logs`.
|
|
|
|
## Automated tests for new logic
|
|
|
|
For any change beyond pure UI tweaks or docs, add automated tests.
|
|
"Tested manually" alone is not enough; it doesn't survive the next
|
|
refactor, and reviewers can't re-verify it.
|
|
|
|
Minimum bar:
|
|
|
|
- **New logic in `quartz/`** (event types, NIPs, parsing, crypto,
|
|
Bech32) — must have unit tests in the matching
|
|
`commonTest` / `androidTest` / `jvmTest` source set. Quartz is the
|
|
protocol surface; everything new there gets coverage.
|
|
- **New logic in `commons/`** (ViewModels, filters, formatters,
|
|
non-trivial state transitions) — unit tests for the paths a future
|
|
refactor could break.
|
|
- **Bug fixes** — a regression test that fails before your fix and
|
|
passes after. No exception. If the bug is hard to reproduce in a
|
|
unit test, write the test that reproduces it first.
|
|
- **UI-only changes** in `amethyst/` or `desktopApp/` — automated UI
|
|
tests are not required (per `CONTRIBUTING.md` § Tests). The manual
|
|
on-device test plan and screenshots stay required.
|
|
|
|
If your change touches a domain covered by an interop suite (MLS /
|
|
Marmot, NIP-17 DMs, audio rooms, MoQ-lite, QUIC), run the relevant
|
|
suite locally and paste the result. CI does not run them. See
|
|
[`CONTRIBUTING.md` § *Interoperability tests*](CONTRIBUTING.md#interoperability-tests)
|
|
for the suite list and commands.
|
|
|
|
Commands:
|
|
|
|
```bash
|
|
./gradlew test # unit + KMP common tests, all modules
|
|
./gradlew :quartz:test # one module
|
|
./gradlew connectedAndroidTest # Android instrumented (needs device)
|
|
```
|
|
|
|
Tests pass before you open the PR. "CI will catch it" is not a
|
|
substitute — interop and instrumented suites don't run in CI.
|
|
|
|
## Regression test plan
|
|
|
|
The PR template has a **Test plan** section. For AI-authored PRs that
|
|
aren't pure docs or translations, that section must contain *two*
|
|
parts under these exact subheadings:
|
|
|
|
- `### Feature test plan` — what you did to confirm the new thing works.
|
|
- `### Regression test plan` — what you did to confirm the old things
|
|
still work, and that you actually thought about which ones could
|
|
break.
|
|
|
|
Don't add a new top-level section to the PR description — put both
|
|
subheadings inside the existing **Test plan** section.
|
|
|
|
For the regression test plan, list:
|
|
|
|
1. **Touch points** — screens, flows, ViewModels, shared state, or
|
|
modules your change reads from or modifies. List the ones a
|
|
careful reader would expect to be affected, not the entire app.
|
|
2. **Failure mode** — for each touch point, what would actually go
|
|
wrong if your change is buggy. "Feed wouldn't load." "Metadata
|
|
stale across account switch." "OOM on scroll."
|
|
3. **Verification** — what you did to confirm it still works. Same
|
|
`action → observed result` format as the feature test plan.
|
|
|
|
Worked example, for "add a new field to `Account`":
|
|
|
|
- Account creation — could crash on first launch — verified: created
|
|
a fresh npub, app opened home feed.
|
|
- Account switching — could leak state across users — verified:
|
|
switched twice between two npubs, feeds refreshed.
|
|
- Settings export/import — could corrupt restore — verified:
|
|
exported, wiped data, re-imported, no errors.
|
|
|
|
Common touch-point categories worth scanning every PR for:
|
|
|
|
- Account / login / logout / multi-account switch.
|
|
- Both flavours (Play and F-Droid).
|
|
- Feed types: home, profile, hashtag, bookmarks, notifications,
|
|
DMs, communities.
|
|
- Orientation changes.
|
|
- Cold start vs warm start.
|
|
- Background → foreground transitions.
|
|
|
|
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
|
|
access), state so and explain why you accept the risk. A reviewer
|
|
can tell you to do it anyway, but silent omission is not an option.
|
|
|
|
## Code review pass before opening the PR
|
|
|
|
Before pushing, run a code-review pass with a *different* agent or
|
|
model than the one that wrote the code. AI agents are bad at finding
|
|
their own bugs; switching agent breaks the same-context blind spots
|
|
that produced the initial diff.
|
|
|
|
Options:
|
|
|
|
- Use a dedicated review skill if your harness has one — `/simplify`,
|
|
`/kotlin-review`, `/security-review`, `/code-review`.
|
|
- Spawn a fresh agent from a different model (Sonnet → Opus, Opus →
|
|
GPT-5, Claude → Codex) and have it review the diff.
|
|
- Run any static-analysis pass available (`./gradlew lint`).
|
|
|
|
After the review, **re-run the tests and the manual on-device test
|
|
plan**. Review feedback routinely surfaces bugs the tests didn't
|
|
catch; the fix introduces its own risk; verify the fix didn't
|
|
regress.
|
|
|
|
If the review flags issues, either address them or document in the
|
|
PR description why you accept the risk. Don't silently discard
|
|
review output.
|
|
|
|
## Don't touch without an issue first
|
|
|
|
Open an issue and get explicit maintainer alignment before opening a
|
|
PR for:
|
|
|
|
- **Signer and KeyStore surface** — `NostrSigner` and its
|
|
implementations (`NostrSignerInternal`, `NostrSignerRemote`,
|
|
`NostrSignerExternal`), anything touching key storage or the
|
|
signing flow.
|
|
- **Release pipeline** — workflow files under `.github/workflows/`,
|
|
signing config, Gradle plugins, packaging.
|
|
- **NIP direction calls** — anything that changes how Amethyst
|
|
interprets a NIP, or invents a new tag or kind, needs upstream NIP
|
|
discussion (and likely a NIP PR) first.
|
|
|
|
These areas have a high cost when an unaligned PR lands — security
|
|
risk, release breakage, protocol fragmentation. Open the issue first
|
|
regardless of whether you call your change a bug fix, refactor, or
|
|
feature; describe what you observed and what you propose. A maintainer
|
|
will tell you to skip the issue gate if the change is genuinely
|
|
trivial.
|
|
|
|
## Everything else
|
|
|
|
For commit format, dev setup, interop tests, PR structure, translation
|
|
flow, and coding standards — see [`CONTRIBUTING.md`](CONTRIBUTING.md)
|
|
and [`.github/PULL_REQUEST_TEMPLATE.md`](.github/PULL_REQUEST_TEMPLATE.md).
|