# 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/` for additive work, `fix/` for bug fixes, `docs/` for documentation-only work. The driver sessions use `feature/v-` (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 . ### Added - **** — what it does, where it lives, why it exists. Include file paths inline so a reader can click through. ### Changed - **** — what changed and how a deployment notices. ### Migration - **`.sql`** — auto-applied by `db.run_migrations()` on backend start. ### 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__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 `