Files
rfc-app/CONTRIBUTING.md
Ben Stull 213f6862d5 docs: CONTRIBUTING.md + SPEC.md §21 analytics chapter
Lands roadmap item #19 (CONTRIBUTING + transcript-linked onboarding)
and #21 Part B (standing analytics instrumentation discipline) as a
single docs commit. No code change, no version bump — these ride a
future release or land as a no-bump docs commit at the operator's
discretion.

CONTRIBUTING.md (new):
  - Project-evolution framing pointing at wiggleverse/ohm-session-history
    as the authoritative how-we-got-here record.
  - Worked-example links into Sessions E (clean small release),
    I (recovery from deploy fault), K (multi-feature wave with
    operator-secret pause), L (squash-merge across three parallel
    features + #21 Part C identity-lifecycle wiring).
  - PR-shape contract: subagents push feature branches; operator
    tags + deploys (the pattern driver sessions use).
  - CHANGELOG strict-descending convention.
  - RFC 2119 keyword discipline for Upgrade-steps blocks (per §20.4).
  - §19.2 candidate discipline — architectural deferrals get noted
    rather than scope-creeping a release.
  - Test-coverage expectations characterized from the actual
    ~250-test backend pytest surface (no frontend test runner today;
    the discipline is build-clean + backend HTTP contract coverage).
  - Operator-only gestures explicitly enumerated (no tagging, no
    deploying, no pin moves, no secret bytes in conversation).
  - Analytics instrumentation checklist (#21 Part B's CONTRIBUTING
    artifact): named events, autocapture-friendly DOM, replay
    masking, PR description discipline.

SPEC.md §21 (new chapter — Analytics instrumentation and identity):
  - Placed AFTER §20 versioning rather than between §15 and §16, to
    avoid renumbering §19.2 / §19.3 — those numbers are load-bearing
    project nouns referenced from CLAUDE.md, transcripts, and prior
    commits. §15 carries a new forward-pointer naming §21 as the
    peer chapter.
  - §21.1 event-taxonomy conventions (Title Case "Subject Verb",
    snake_case props, no PII).
  - §21.2 required prop families per event kind, including the
    hashed-target_email convention for invite-side events that
    target a not-yet-user (#12).
  - §21.3 autocapture-friendly DOM patterns (stable text,
    aria-label on icon-only buttons, data-amp-track-* on
    repeated rows, data-amp-track-suppress for noise surfaces).
  - §21.4 session-replay masking — credentials MUST be masked,
    PII SHOULD be masked, privacy policy MUST match reality.
  - §21.5 consent-gate contract — pre-consent no init / no
    network / no recording; granted→denied → setOptOut(true)
    within one tick.
  - §21.6 identity lifecycle (per #21 Part C) — identify with
    user_id + properties on sign-in; setUserProperties on
    mid-session change; anonymize on sign-out (after the
    User Signed Out track); identify-BEFORE-track on invite-claim
    paths. §21.6.1 set vs setOnce taxonomy with explicit classification
    rule.
  - §21.7 cohort-shape implications (informative — what the
    Amplitude dashboard can actually answer with the conventions).
  - §21.8 secret-vs-public framing for the overlay binding —
    Amplitude browser key is public (bundle-embedded), Turnstile
    secret half is real-secret; canonical pbpaste-pipe gesture
    for the operator.
  - §21.9 new §19.2 candidates surfaced (session-replay-specific
    consent category, bundle-size budget measurement, property-
    shape CI lint, centralized email-hash helper).
  - §21.10 open question (consent-category split timing).

Grounded in real code: the SPEC chapter cites
frontend/src/lib/analytics.js v0.15.0 + the v0.16.0 (AcceptInvitation)
+ v0.17.0 (InviteClaim) inline #21 Part C wiring as worked examples.
RFC 2119 keywords used precisely throughout.
2026-05-28 05:39:46 -07:00

18 KiB

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).


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 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) — 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 — 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):

## 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 / RFC 8174 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/:

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). By contributing, you agree your work ships under those terms.


See also

  • 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 — release history in strict descending order. Read recent entries for the shape your PR's release-commit will take.
  • 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 — the authoritative record of how the project has actually evolved, session by session.