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.
This commit is contained in:
+407
@@ -0,0 +1,407 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user