# Contributing to rfc-app

`rfc-app` is the framework that hosts RFC-shaped collections of
documents — one repo per RFC, a meta repo per collection, a web app
that turns the Git substrate into a writeable surface. The Open
Human Model (OHM) deployment at `ohm.wiggleverse.org` is one
instance. The framework is intended to host more.

This document explains how to propose a change to the framework
itself — a new endpoint, a schema migration, a UI affordance, a
spec clarification. For changes to *content* hosted by a specific
deployment (the OHM RFCs, the OHM roadmap), see that deployment's
own contribution guide (e.g. [`ohm-rfc/CONTRIBUTING.md`](https://git.wiggleverse.org/wiggleverse/ohm-rfc/src/branch/main/CONTRIBUTING.md)).

---

## How the project actually evolves

rfc-app is built in the open in the literal sense: **every build
session produces a full transcript** at
[`wiggleverse/ohm-session-history`](https://git.wiggleverse.org/wiggleverse/ohm-session-history)
on `git.wiggleverse.org`. The transcripts are the authoritative
record of how the framework got from one release to the next — the
decisions, the friction, the dead ends, the reasoning. They are not
curated retrospectives; wrong turns stay in.

If you are proposing a change to rfc-app, **read at least the most
recent session transcript before opening a PR.** The transcripts
show what shape a feature lands in, where the spec gets touched,
what the operator pushes back on, and how the release rides into
deployment. A PR that matches that texture is much more likely to
land cleanly than one shaped by the README alone.

Worked examples to start with:

- **Session E** ([transcript](https://git.wiggleverse.org/wiggleverse/ohm-session-history)) —
  a clean small release. Read this for the simplest possible release
  shape: one feature, one version bump, one upgrade-steps block, no
  surprises.
- **Session I** — recovery from a deploy fault. Read this for how
  the project handles things going wrong mid-deploy, and for the
  honest no-curation discipline.
- **Session K** — a multi-feature wave with one item paused on an
  operator-provided secret. Read this for the subagent dispatch
  pattern (the model the project uses to ship multiple features in
  parallel), and for the binding rule that the assistant **never**
  asks the operator to paste secret bytes into the conversation.
- **Session L** — squash-merge integration across three parallel
  features (v0.15.0 / v0.16.0 / v0.17.0), with `#21 Part C`
  identity-lifecycle Amplitude wiring folded inline across all
  three releases. Read this for how cross-cutting concerns (analytics,
  observability) get layered into already-in-flight features
  without scope-creeping any single release.

The repository where transcripts live —
[`wiggleverse/ohm-session-history`](https://git.wiggleverse.org/wiggleverse/ohm-session-history) —
is the canonical history. The `git log` of `rfc-app` is the artifact;
the transcripts are the story behind it.

---

## How a contribution flows

The framework runs on a **subagents push feature branches; operator
tags and deploys** model. Contributors — whether human or AI agents
running in a Claude Code subsession — open feature branches and
submit PRs. The operator (the person running the deployment) is the
one who merges, tags, bumps `VERSION`, runs `flotilla deploy` (or
the equivalent for non-OHM deployments), and moves the deployment's
`.rfc-app-version` pin. The driver session transcripts inherit
this shape; contributors inherit it from them.

Concretely:

1. Read the most recent session transcript. Understand what just
   shipped and what is in flight.
2. Open an Issue first if your change is exploratory, structural,
   or might overlap with in-flight work. The operator will name
   any collision.
3. Branch from `main`. Name the branch
   `feature/<short-description>` for additive work, `fix/<short-
   description>` for bug fixes, `docs/<short-description>` for
   documentation-only work. The driver sessions use
   `feature/v<target-version>-<slug>` (e.g.
   `feature/v0.16.0-owner-invite`) — that shape is welcome but not
   required for outside contributors, since contributors do not
   pick the target version.
4. **Do not bump `VERSION` or `frontend/package.json#version` in
   your PR.** The operator picks the target version at integration
   time; bumping ahead causes cherry-pick conflicts. The same
   applies to the `CHANGELOG.md` entry header — see below.
5. **Do not tag releases, do not run any deploy gesture, do not
   touch any deployment's `.rfc-app-version` pin.** The operator
   alone owns those gestures. (For OHM specifically: "I'm the only
   one that gets to yolo." See the boundary section in
   `ohm-rfc/CONTRIBUTING.md`.)
6. Push your branch and open a PR. Describe what you're proposing
   and why, in language the operator can paste into the eventual
   release commit. If the change touches `SPEC.md`, name which
   section(s) and the contract change.

---

## CHANGELOG convention: strict descending

`CHANGELOG.md` is ordered **newest-on-top**. The header line for
the in-progress version goes at the top of the file; older
releases descend below it. This is the binding convention; the
operator hand-resolves the conflict when two parallel feature
branches both insert at the top of the file (the squash-merge
integration that ships parallel-feature waves keeps the strict-
descending shape — see Session K for the cherry-pick mechanics and
Session L for the hand-resolved-with-a-small-script variant).

A new entry has this shape (read the existing 0.15.0 / 0.16.0 /
0.17.0 entries for worked examples):

```markdown
## 0.X.Y — YYYY-MM-DD

**Minor — schema migration auto-applied; no operator action.** This
release ships <one or two sentences naming the feature and why>.

### Added
- **<New module/endpoint/component>** — what it does, where it lives,
  why it exists. Include file paths inline so a reader can click through.
### Changed
- **<Existing surface>** — what changed and how a deployment notices.
### Migration
- **`<NNN_name>.sql`** — auto-applied by `db.run_migrations()` on
  backend start. <Describe the schema delta in one sentence.>
### Upgrade steps (from 0.(X-1).Y)
- You **MUST** … (per RFC 2119; see SPEC.md §20.4).
- You **MUST NOT** …
- You **SHOULD** …
- You **MAY** …
```

The header version number is filled in by the operator at merge
time. Your PR's CHANGELOG diff can leave the version as
`0.X.Y — YYYY-MM-DD` (literal placeholder), or use a guessed value
the operator overwrites; either is fine.

---

## `Upgrade steps:` blocks use RFC 2119 keywords

If your change requires deployments to do anything when they
upgrade — set an env var, apply a migration, restart a process,
flip an overlay value, accept a behavioral change — your CHANGELOG
entry **must** include an `### Upgrade steps` block, and that
block **must** use the [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)
/ [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174) keywords as
defined in `SPEC.md` §20.4:

- **MUST** / **SHALL** / **REQUIRED** — without this step the
  deployment will not function correctly. Skipping is a regression
  the framework does not handle.
- **MUST NOT** / **SHALL NOT** — previously valid, now no longer
  supported.
- **SHOULD** / **RECOMMENDED** — the framework's tested path. A
  deployment may deviate when it has a reason.
- **SHOULD NOT** / **NOT RECOMMENDED** — discouraged without being
  forbidden.
- **MAY** / **OPTIONAL** — an affordance you can take or skip.

Cross-version upgrades (jumping more than one minor) are computed by
the operator composing each intervening release's steps in order.
Each adjacent step must therefore be locally unambiguous — this is
the whole reason the keyword discipline is binding. Avoid words
like "should probably" or "might want to" inside an upgrade step;
either the framework needs the action or it doesn't.

If your change touches the env contract, **also update**
`backend/.env.example` and/or `frontend/.env.example` in the same
PR so the contract and the documentation land together (§20.4).

---

## SPEC.md and §19.2 candidates

`SPEC.md` is the framework's binding spec. It is honest about open
questions — large sections of it carry "§19.2 candidates," which
are decisions the project has deliberately deferred rather than
guessed at.

The discipline: **architectural or process deferrals get noted as
§19.2 candidates rather than scope-creeping a release.** When you
notice that your change opens a question larger than the change
itself (a different DB shape, a new auth contract, a cross-cutting
UX rethink), the right move is usually to land the narrow change
and add a §19.2 candidate naming the larger question. The candidate
documents what was set aside and why, so a future session can pick
it up with context.

Worked examples from recent sessions:

- v0.11.0 (Session K) shipped device trust and surfaced three new
  §19.2 candidates: cross-device session revocation, password-
  equivalent change invalidating trust, device-trust window
  tunables via env. None of those were in the v0.11.0 scope; they
  were noted in SPEC.md §19.2 so a future session can address them
  on their own terms.
- v0.15.0 (Session L) shipped the Amplitude wrapper and added
  candidates around session-replay-specific consent category +
  bundle-size measurement, both deferred to the future Part-A audit.

When you spot a deferred decision in your PR's territory, name it
in your PR description and add it to `SPEC.md` §19.2 in the same
diff. Do not silently expand scope to settle it.

---

## Test-coverage expectations

The backend has the load-bearing test suite at
`backend/tests/`. Tests are organized as `*_vertical.py` files,
each covering one feature end-to-end through the FastAPI app
(provisioning fixtures, hitting the HTTP surface, asserting on the
database state). At time of writing, the suite is ~250 tests across
~25 files. Examples:

- `test_admin_create_user_invite_vertical.py` — v0.17.0's
  admin-create user + invite + claim flow, 15 tests covering happy
  path + every refusal shape + the audit-trail row.
- `test_rfc_invitations_vertical.py` — v0.16.0's per-RFC invite +
  accept flow, 18 tests.
- `test_device_trust_vertical.py` — v0.11.0's 30-day device trust,
  14 tests including cookie shape, hash-vs-raw-token discipline,
  expired / revoked / forged / cross-user invariants.

Expected coverage for a new feature:

- **Backend feature** — one new `test_<feature>_vertical.py` file
  that covers the happy path, every documented refusal/error code,
  and any cross-surface effect (rows the feature writes to existing
  tables, fields it adds to existing endpoints). Reuse fixtures
  from neighboring test files (e.g. `test_propose_vertical.py`'s
  `FakeGitea` is widely reused).
- **Migration** — verify migrations are reachable from `backend/.venv`
  before pushing: `cd backend && PYTHONPATH=. .venv/bin/pytest -q`
  exercises `db.run_migrations()` through the fixture setup.
- **Frontend feature** — there is currently no frontend test
  runner. The discipline is: keep the change ships-clean
  (`cd frontend && npm run build` succeeds), and the backend
  vertical test exercises the HTTP contract the frontend
  consumes, which is the meaningful behavioral guarantee.
  Frontend changes that ride along with a backend feature land
  with the backend test as the regression boundary.
- **Bug fix** — add a regression test in the same vertical file
  that proves the original failure mode and verifies the fix.

Run the backend suite before pushing. From `backend/`:

```bash
PYTHONPATH=. .venv/bin/pytest -q
```

(The `PYTHONPATH=.` is a known ergonomic gap — see SPEC.md §19.2
candidate; the suite does not pick up `app/` without it.)

If your PR doesn't include tests, the operator will ask for them
before merge unless the change is genuinely test-irrelevant
(documentation, comments, dev-only tooling).

---

## Analytics instrumentation checklist

> *(This section codifies `ohm-rfc/ROADMAP.md` #21 Part B's
> CONTRIBUTING checklist. It is discipline, not a gate — but the
> operator will push back on PRs that skip it.)*

If your PR adds or changes a user-facing feature, walk this
checklist before opening the PR. The instrumentation conventions
themselves are specified in `SPEC.md` §21 (Analytics instrumentation
and identity); this section is the procedural reminder.

1. **What named event(s) does this feature need?**
   Open `frontend/src/lib/analytics.js` and look at the `EVENTS`
   constant. Does an existing event cover your feature? If not, is
   the new event in the spec's "Subject Verb" Title Case form
   (`Comment Posted`, `Invitation Sent`)? Are the prop families
   consistent with SPEC.md §21's required-prop catalog (opaque
   ids only, no PII, enums lowercased like `'otc'` not `'OTC'`)?

2. **Do interactive elements have stable text / ARIA labels /
   `data-amp-track-*` so autocapture is meaningful?**
   The frontend ships `autocapture: true`, which instruments
   every click and form interaction. The *value* of those events
   depends on the DOM the SDK sees: a `<button>` with stable
   visible text or an `aria-label` shows up as a meaningful
   dashboard row; an icon-only `<button>` with no label shows up
   as garbage. New components that introduce interactive elements
   should either carry meaningful labels (visible text or ARIA) or
   carry a `data-amp-track-name="<Stable Name>"` attribute. For
   repeated rows (per-RFC lists, comment lists), use a stable
   `data-amp-track-*` identifier so per-row click counts aggregate
   to the row's identity rather than to a generic label.

3. **Does any new form field need replay masking?**
   Session replay records at `sampleRate: 1` (100% of consented
   sessions). New form inputs that capture passwords, OTC codes,
   tokens, magic-link URLs, or other secret/credential-equivalent
   material **MUST** be masked with Amplitude's masking conventions
   (the `.amp-mask` class or the `data-amp-mask` attribute,
   whichever the wrapper integration expects in this version).
   New inputs that capture arguably-PII (email, real name, free-
   text drafts) **SHOULD** also be masked; if a deliberate
   un-masking decision is taken, document it in the PR description
   and in `SPEC.md` §21.

4. **Does the PR description name the instrumentation decisions?**
   A one-sentence summary in the PR description — "fires
   `Comment Posted` with `{rfc_slug, comment_id}`; no new form
   fields, no new replay-masking concerns" — is enough. If the
   decision is "we chose not to instrument this," say that too;
   the absence of an event is itself a decision the operator
   wants visible. The relevant SPEC chapter (§21) is the binding
   reference for what shapes are correct.

If your feature touches an identity-meaningful surface (sign-in,
sign-out, invite-claim, role change, account state change), also
walk the **identity lifecycle** contract in SPEC.md §21.6: every
new claim/sign-in path **MUST** call `identify({ user_id, properties })`
BEFORE the first `track()` event on that surface, so the Amplitude
user record is created with the OHM user_id from the very first
event rather than as an anonymous device that retroactively links.
v0.16.0's `AcceptInvitation.jsx` and v0.17.0's `InviteClaim.jsx`
are the worked examples; mirror their shape.

---

## The operator-only gestures

Some gestures are operator-only. Contributors do not perform them;
PRs that perform them get rejected on principle, not on merit:

- **Tagging a release** (`git tag v0.X.Y` + `git push --tags`).
- **Pushing to `main`** after merge (the operator merges; the
  framework's `main` branch tracks releases the operator has
  shipped).
- **Bumping `VERSION` and `frontend/package.json#version` to the
  shipped value.** The operator does this at integration time so
  the version line is consistent across the release commit.
- **Running `flotilla deploy` or any equivalent deployment gesture**
  in any deployment of rfc-app. Contributors do not deploy.
- **Moving a deployment's `.rfc-app-version` pin.** That pin lives
  in the deployment's content repo (e.g. `ohm-rfc/.rfc-app-version`)
  and is moved by the deployment's operator. Contributors to that
  deployment do not move it; contributors to the framework
  certainly do not.
- **Setting secrets** (anywhere — Secret Manager, env files,
  `flotilla secret set`, vendor dashboards, anything). The
  binding rule baked in mid-Session-K is: **the assistant never
  asks the operator to paste secret bytes into a conversation,
  even as one offered option**. The corollary for contributors:
  do not include secret values in PR descriptions, commit
  messages, or issue comments. Reference secrets by their binding
  name (`SMTP_PASSWORD`, `AMPLITUDE_API_KEY`) and let the
  operator handle the bytes.

If your change requires a new secret or env var, document the
requirement in the CHANGELOG `### Upgrade steps` block in the
RFC 2119 form ("operators **MUST** set `<NEW_VAR>` ...") and
update the `*.env.example` file. The operator will run the
secret/overlay-set gesture themselves at deploy time.

---

## When in doubt

- **Open an Issue first.** Especially for any change that touches
  SPEC.md, the auth/permissions model (§6), the storage shape (§4
  / §5), or the deploy contract (§20). The operator (or a future
  driver session) will name what they want before you write code.
- **Read the most recent session transcript.** It will tell you
  what shipped last and what's in flight.
- **Cite SPEC.md sections in your PR description.** "Touches §15.4
  (per-category email toggles) and adds §19.2 candidate around
  per-channel mute granularity" gives the operator a map of where
  to read.

---

## License

The framework is released under the MIT License (see
[`LICENSE`](./LICENSE)). By contributing, you agree your work
ships under those terms.

---

## See also

- [`SPEC.md`](./SPEC.md) — the framework's binding spec. §19.2
  is the deferred-decisions queue; §20 is the versioning + deploy
  contract; §21 is the analytics instrumentation contract.
- [`CHANGELOG.md`](./CHANGELOG.md) — release history in strict
  descending order. Read recent entries for the shape your PR's
  release-commit will take.
- [`PHILOSOPHY.md`](./PHILOSOPHY.md) — what the framework is for.
  PRs whose shape conflicts with the philosophy get a longer
  conversation than PRs that fit.
- [`wiggleverse/ohm-session-history`](https://git.wiggleverse.org/wiggleverse/ohm-session-history)
  — the authoritative record of how the project has actually
  evolved, session by session.
