mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89856df37f | ||
|
|
d3a2f5deda |
@@ -10,6 +10,9 @@ public/config.json
|
||||
backend/synapse_tmp/*
|
||||
backend/synapse_tmp_othersite/*
|
||||
/coverage
|
||||
|
||||
# Transient agent scratch files
|
||||
/agent-workspace/
|
||||
config.json
|
||||
|
||||
# Yarn
|
||||
|
||||
+1
-1
@@ -2,5 +2,5 @@
|
||||
"$schema": "./node_modules/oxfmt/configuration_schema.json",
|
||||
"printWidth": 80,
|
||||
"sortPackageJson": false,
|
||||
"ignorePatterns": ["pnpm-lock.yaml", "node_modules", "dist"]
|
||||
"ignorePatterns": ["pnpm-lock.yaml", "node_modules", "dist", "FEATURES_SPEC"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# AGENTS.md — Element Call
|
||||
|
||||
MatrixRTC (MSC4143) + LiveKit video calling, shipped standalone, as a widget in
|
||||
Element Web and Element X, as embedded packages, and as a React component in a
|
||||
host's page. It is the MatrixRTC reference implementation.
|
||||
|
||||
## Non-negotiables
|
||||
|
||||
- Features and UI changes need a pre-approved issue. No issue, no review.
|
||||
- Exactly one `PR-*` label per PR. CI enforces it; it drives the changelog.
|
||||
- New behaviour ships with unit tests, a story if it renders, and an e2e spec if it
|
||||
is user-facing. All three.
|
||||
- Reuse an existing component. If you genuinely cannot, say so in the PR body and
|
||||
name what you rejected — never add a new one silently.
|
||||
- Nothing reads the page. No `window.location`, `document.body`, `window.inner*`,
|
||||
`@media`, global `i18next` or the `widget` global — take it from a provider.
|
||||
- The change works standalone, as a widget and as a component. Say what you checked.
|
||||
- Every gate below is green before you push.
|
||||
- Hand off after the first implementation, before the quality pass. Commit when the
|
||||
user confirms direction, not before.
|
||||
|
||||
## Read before you
|
||||
|
||||
| … | … |
|
||||
| -------------------------------- | ---------------------------------------------------------- |
|
||||
| start a task of any size | [docs/agents/workflow.md](docs/agents/workflow.md) |
|
||||
| touch call logic or a view model | [docs/agents/architecture.md](docs/agents/architecture.md) |
|
||||
| write any code | [docs/agents/code-style.md](docs/agents/code-style.md) |
|
||||
| write a test, story or e2e spec | [docs/agents/testing.md](docs/agents/testing.md) |
|
||||
| open a PR | [CONTRIBUTING.md](CONTRIBUTING.md) |
|
||||
| implement from a feature spec | [FEATURES_SPEC/AGENTS.md](FEATURES_SPEC/AGENTS.md) |
|
||||
|
||||
## Gates
|
||||
|
||||
```sh
|
||||
pnpm lint # tsc, oxlint, knip, component externals
|
||||
pnpm format # oxfmt
|
||||
pnpm test # vitest: unit and storybook projects
|
||||
pnpm i18n:check
|
||||
```
|
||||
|
||||
`knip` fails on dead code; a file deliberately inert ahead of its consumer goes in
|
||||
`knip.ts` `ignoreFiles` with a reason.
|
||||
|
||||
## A good PR
|
||||
|
||||
- One slice, under ~400 changed lines, green on its own, linked to its issue.
|
||||
- View model + marble tests, thin view, a story per state, an e2e spec.
|
||||
- Any new shared component called out explicitly, with why nothing existing fit.
|
||||
- Template filled for real: what, why, before/after screenshots, repro steps.
|
||||
- Branch `<handle>/<topic>`. Plain imperative commit subjects, no prefixes.
|
||||
- Once review starts, fix forward. Never force-push a regeneration over a review.
|
||||
@@ -0,0 +1,415 @@
|
||||
# FEATURE_SPEC — Agent Instructions
|
||||
|
||||
This folder holds **feature specs distilled from experiments**. A spec is the durable
|
||||
artifact of an exploration; the exploration branch is not. Each spec must contain
|
||||
enough information for an agent to produce mergeable PRs against a *current* `main`
|
||||
without access to the original branch.
|
||||
|
||||
## What this folder is not
|
||||
|
||||
- Not a design-doc archive. Specs that are not intended to be implemented do not
|
||||
belong here.
|
||||
- Not a place for narrative. Prose that does not constrain the implementation or its
|
||||
verification should be cut.
|
||||
- Not a substitute for the PR description. The spec explains *why* and *what must be
|
||||
true*; the PR explains *this diff*.
|
||||
|
||||
## File naming
|
||||
|
||||
`YYYY-MM_Snake_Case_Title.md` — e.g. `2026-09_Raise_Hand_Improvements.md`
|
||||
|
||||
The filename is immutable once committed. Renames break links from PRs, issues and
|
||||
the drift log. If scope changes fundamentally, write a new spec and set
|
||||
`supersedes:` / `superseded_by:`.
|
||||
|
||||
A sidecar `YYYY-MM_Snake_Case_Title.product.md` holds the product spec — user stories,
|
||||
functional requirements, success criteria. Copy `_TEMPLATE.product.md`. Product owns
|
||||
it; see "Brainstorming: the product spec".
|
||||
|
||||
An optional sidecar `YYYY-MM_Snake_Case_Title.notes.md` holds lessons learned,
|
||||
narrative, dead-end logs and anything else that is worth keeping but does not
|
||||
constrain the implementation. Rules:
|
||||
|
||||
- The sidecar is **never load-bearing**. Agents do not read it to derive
|
||||
implementation, and nothing in it may contradict or extend the spec. If a note
|
||||
constrains the work, it belongs in `## Decisions`, `## Rejected alternatives` or
|
||||
`## Measurements` instead.
|
||||
- Agents may append to it (an observation from a drift check, a wrong turn during
|
||||
implementation) and must never rewrite or delete existing content.
|
||||
- An agent that is `blocked` may read the sidecar to draft an open question, but
|
||||
quotes what it found rather than acting on it.
|
||||
- No sidecar is required. An empty one is worse than none.
|
||||
|
||||
## Style: facts only
|
||||
|
||||
Applies to the spec file only. The product spec is prose by design and the notes
|
||||
sidecar is unconstrained. The spec is re-read on every regeneration, where prose costs context
|
||||
and dilutes the contract, so:
|
||||
|
||||
- Bullets, not paragraphs. Declarative present tense. No motivation, no background,
|
||||
no summary of what the code currently does beyond locating the change.
|
||||
- Name things instead of describing them — a path, a symbol, an event type, a command,
|
||||
an event id. A reference is shorter and more precise than a sentence about it. Which
|
||||
kind of reference is allowed depends on the section; see "Reference durability".
|
||||
- One line per decision plus at most one sentence of rationale. If the rationale needs
|
||||
more, the extra goes in the sidecar.
|
||||
- No judgement adjectives (clean, elegant, proper, obviously) and no hedging (should
|
||||
probably, might be nice). Either it is a constraint or it is not in the spec.
|
||||
- `## Problem` is at most five bullets.
|
||||
- Target under 200 lines for the whole spec; over 300 lines, split the feature or move
|
||||
material to the sidecar.
|
||||
|
||||
The test for any line: does it constrain the implementation, constrain its
|
||||
verification, or record a measured fact? If not, delete it or move it to the sidecar.
|
||||
|
||||
## Reference durability
|
||||
|
||||
The spec must stay implementable against a `main` nobody has seen yet, so which
|
||||
references are allowed depends on how fast they rot. Most to least durable:
|
||||
|
||||
1. Protocol identifiers — `m.reaction`, `m.call.member`, `m.relates_to`, a reaction
|
||||
key. MSC-bound; effectively permanent.
|
||||
2. Observable behaviour. Permanent.
|
||||
3. Concept names — membership, participation, session.
|
||||
4. Test names. Survive file moves and renames, because the test moves with the code.
|
||||
5. Exported symbols. Survive refactors sometimes.
|
||||
6. File paths. Rot fast.
|
||||
7. Line numbers. Never.
|
||||
|
||||
Per section:
|
||||
|
||||
- `## Problem`, `## Measurements`, `## Slicing plan`, `## Drift log` — any reference,
|
||||
paths and internal symbols included. These describe the codebase **as of `base`**. A
|
||||
reference going stale here is a finding for the drift check, not a defect, and
|
||||
removing the concrete references would leave the drift check with nothing to verify.
|
||||
- `## Decisions`, `## Acceptance criteria` — levels 1 to 4 only. No file paths, no
|
||||
internal symbols. This is the part that has to hold against a future `main`.
|
||||
- An acceptance criterion that is not user-observable is tagged `[internal]` and
|
||||
anchored by **its test name**, not by the symbol under test. The test name moves with
|
||||
a refactor; the symbol does not.
|
||||
|
||||
`[internal]` ACs are legitimate and expected: an invariant that no current consumer can
|
||||
observe is exactly what protects the consumer that does not exist yet. Tagging them
|
||||
keeps visible which ACs defend a product requirement and which defend a code
|
||||
invariant.
|
||||
|
||||
## Required frontmatter
|
||||
|
||||
```yaml
|
||||
---
|
||||
status: draft # see state machine below
|
||||
blocked_from: null # state to return to when leaving `blocked`
|
||||
title: Raise hand improvements
|
||||
owner: <human handle> # a human, always
|
||||
signoff: # join condition for leaving exploration.signoff
|
||||
eng: pending
|
||||
design: pending
|
||||
product: pending
|
||||
experiment_ref: <branch name or commit sha of the spike, if any>
|
||||
base: <sha of main the spec was validated against>
|
||||
repos: [matrix-js-sdk, element-call] # in dependency order
|
||||
specs: [MSC4143] # protocol documents touched, if any
|
||||
supersedes: []
|
||||
superseded_by: null
|
||||
---
|
||||
```
|
||||
|
||||
`base` is mandatory and is what makes drift detectable. `owner` is a human even when
|
||||
every section was drafted by an agent.
|
||||
|
||||
## Brainstorming: the product spec
|
||||
|
||||
The usual entry point is a human handing an agent a one-line feature idea. The output
|
||||
of that session is `<stem>.product.md`, not code and not a feature spec. Copy
|
||||
`_TEMPLATE.product.md` and fill it.
|
||||
|
||||
Ownership: an agent may create and fill this file during `brainstorm`. From `draft`
|
||||
onwards **product owns it and agents do not edit it** — not to fix wording, not to
|
||||
resolve a marker, not to reconcile it with the code. Agents raise mismatches in the
|
||||
feature spec's `## Open questions`.
|
||||
|
||||
If product's source of truth lives outside the repo (a tracker, a design doc), put its
|
||||
URL in the header `**Source**:` line and keep the requirement text and ids in the repo
|
||||
anyway. The ids are what the feature spec references; a link alone cannot be traced.
|
||||
|
||||
Agent behaviour during the session:
|
||||
|
||||
- **Ask, do not assume.** An unanswered question is recorded as
|
||||
`[NEEDS CLARIFICATION: <the question>]`, inline where the answer would go. Filling a
|
||||
gap with a plausible guess is the main failure mode of this state — the marker is a
|
||||
deliverable, not a defect.
|
||||
- Ask in one round of at most five questions, ordered by how much the answer changes
|
||||
the shape of the feature. Do not interview.
|
||||
- **No implementation in the product spec.** No file paths, symbols, libraries, event
|
||||
types, API shapes or framework names. If a requirement cannot be stated without them,
|
||||
it is not a product requirement.
|
||||
- Every user story must be independently deployable and demonstrable on its own. A
|
||||
story that only makes sense once another story ships is not a story; merge it.
|
||||
- More than four priority levels means more than one feature. Split before `draft`.
|
||||
- Never invent an SC number. Ask for the target, or mark it
|
||||
`[NEEDS CLARIFICATION]`.
|
||||
|
||||
For protocol and infrastructure work, success criteria may be expressed as observable
|
||||
client behaviour or a wire-level invariant ("a client joining mid-session converges
|
||||
within one sync"), but still never as a code path or an internal API.
|
||||
|
||||
`FR-###` and `SC-###` ids are immutable once assigned. Product may revise the document
|
||||
at any time, including after `in_production`, but a changed requirement gets a **new
|
||||
id** and the old one is marked superseded — never edited in place. Renumbering silently
|
||||
breaks every AC that references it, and there is no way to detect that afterwards.
|
||||
|
||||
A new or changed FR after `implement` does not amend the current spec's scope. It is
|
||||
either a new feature spec or the next iteration; the mapping table simply shows an
|
||||
unmapped FR, which is the intended signal.
|
||||
|
||||
## Traceability
|
||||
|
||||
The product spec states intent; the feature spec states what must be true and how it is
|
||||
checked. They are kept in sync by ids, not by duplication.
|
||||
|
||||
- Every `FR-###` maps to at least one acceptance criterion. Tag the AC with its
|
||||
origin: `AC2 [FR-003] — <observable behaviour>`.
|
||||
- Every `SC-###` maps to an entry in `## Measurements` with a method. An SC with no
|
||||
measurement blocks `exploration.signoff`.
|
||||
- Every user story maps to at least one slice. **Slice order follows story priority**
|
||||
(P1 first) unless a technical dependency overrides it, and the override is recorded
|
||||
in the slicing plan with its reason.
|
||||
- An AC with no `FR` origin is allowed and expected — implementation-level invariants
|
||||
have no counterpart in the product spec. The converse is not: an unmapped FR is an
|
||||
incomplete spec.
|
||||
- Agents do not resolve a mismatch between the two. The product spec wins on intent,
|
||||
the feature spec wins on verification; a genuine contradiction is `blocked`.
|
||||
|
||||
## Status state machine
|
||||
|
||||
Transitions are explicit; an agent may perform only the ones marked (agent).
|
||||
|
||||
```
|
||||
brainstorm ─► draft ─► exploration ─► implement ─► qa ─► in_production
|
||||
│ │ │
|
||||
└──────────────┴────────┴──► blocked ──► (blocked_from)
|
||||
│
|
||||
└──► abandoned
|
||||
```
|
||||
|
||||
Exploration has sub-states, written dotted in `status`:
|
||||
|
||||
```
|
||||
exploration.scoping ─► exploration.iterating ─► exploration.signoff
|
||||
▲ │ │
|
||||
└───────────────────────┴──────────────────────┘
|
||||
```
|
||||
|
||||
- `brainstorm` → `draft`: human. Requires `<stem>.product.md` to exist with at least one
|
||||
P1 story carrying acceptance scenarios, every FR and SC id assigned, and every
|
||||
remaining `[NEEDS CLARIFICATION]` copied into the feature spec's `## Open questions`.
|
||||
- `draft` → `exploration.scoping`: human. Requires `## Problem` and `## Out of scope`.
|
||||
Scoping before the branch exists is what keeps exploration from becoming the
|
||||
long-lived branch this folder replaces.
|
||||
- `exploration.scoping` → `.iterating`: human. `experiment_ref` must name the branch.
|
||||
- `.iterating` → `.signoff`: human. Requires `## Decisions`, `## Acceptance criteria`,
|
||||
`## Rejected alternatives` and `## Measurements` to be complete, no
|
||||
`[NEEDS CLARIFICATION]` left in the product spec, and full traceability (below).
|
||||
- `.signoff` → `implement`: **only when all three sign-offs in `signoff:` are `ok`.**
|
||||
This is a join, not a sequence — a single reviewer cannot advance it. On entering
|
||||
`implement`, the exploration branch stops being a base: it is reference material,
|
||||
and implementation branches off current `main` (see "Turning a spec into PRs").
|
||||
- `.signoff` → `.iterating`: human, on any sign-off returning changes. Reset the
|
||||
rejecting role to `pending`; leave the others as they are.
|
||||
- `implement` → `qa`: (agent) when every acceptance criterion is covered, as recorded
|
||||
in `## PRs`, and the feature flag is enabled in the QA environment.
|
||||
- `qa` → `in_production`: human. The flag is on by default in production.
|
||||
- `qa` → `implement`: (agent) when a QA finding needs code. See "QA feedback".
|
||||
- any active state → `blocked`: (agent) when drift or an unresolved question prevents
|
||||
progress. Requires a drift-log entry and `blocked_from: <state>`. Do not guess and
|
||||
continue. Leaving `blocked` returns to `blocked_from`, never to a fixed state.
|
||||
- any state → `abandoned`: human only.
|
||||
|
||||
`in_production` is terminal. A production regression does not reopen the spec: it
|
||||
either is an ordinary bug fix with no spec, or it is a new spec. The acceptance
|
||||
criteria remain as the regression contract (see "QA feedback").
|
||||
|
||||
## QA feedback
|
||||
|
||||
The point of the `qa` state is not to record that testing happened. It is to make the
|
||||
spec a better regeneration source than it was before implementation. So:
|
||||
|
||||
- Every reproduced QA finding becomes **either** a new acceptance criterion **or** a
|
||||
new entry in `## Rejected alternatives`. Never a prose note, never only a bug
|
||||
tracker link.
|
||||
- (agent) may **append** ACs during `qa`, with provenance:
|
||||
`AC7 [qa: <issue ref>] — <observable behaviour>` plus its check. Appending is
|
||||
allowed; rewriting or reordering existing ACs is not.
|
||||
- A finding that invalidates a decision does not become an AC. It sets `blocked` — a
|
||||
decision is human-owned in every state.
|
||||
- A finding that is out of scope goes to `## Out of scope` with its issue ref, so the
|
||||
next regeneration does not treat it as a gap.
|
||||
|
||||
This also applies after `in_production`: a production bug that reveals a missing AC is
|
||||
appended with `[prod: <ref>]` provenance without any state change.
|
||||
|
||||
## Required sections
|
||||
|
||||
Order matters — agents read top-down and the first three sections are the contract.
|
||||
|
||||
1. **`## Problem`** — up to five bullets. What breaks today, for whom, observably,
|
||||
with the file or symbol where it breaks.
|
||||
2. **`## Decisions`** — the choices the exploration settled, each with a one-line
|
||||
rationale. **Human-owned. An agent must never add, remove or reword a decision.**
|
||||
If a decision is missing, set `blocked` and ask.
|
||||
3. **`## Acceptance criteria`** — the load-bearing section. Each item must be
|
||||
mechanically checkable and name its check:
|
||||
- `AC1 [FR-002] — <observable behaviour>` / `check: yarn vitest run src/reactions/ReactionsReader.test.tsx -t "backfill"`
|
||||
- `AC2 [FR-002] — <observable behaviour>` / `check: manual, <exact steps>`
|
||||
- `AC3 [internal] — <invariant>` / `check: yarn vitest run <file> -t "<test name>"`
|
||||
An acceptance criterion without a check is a wish; reject the spec (`blocked`).
|
||||
4. **`## Rejected alternatives`** — what the experiment tried and why it failed,
|
||||
including measurements. This exists so regeneration does not re-walk dead ends. If
|
||||
this section is empty for a spec with an `experiment_ref`, the spec is incomplete.
|
||||
5. **`## Measurements`** — numbers from the experiment with the method used to obtain
|
||||
them (command, hardware, load profile). Numbers without a method are not usable as
|
||||
regression baselines and must be marked `unverified`. A baseline states its direction: a
|
||||
count that unrelated work grows — tests passing, test files — is a floor (`≥ n`); a
|
||||
cost — latency, CPU, bundle size — is a ceiling (`≤ n`). An exact count is stale as
|
||||
soon as `main` moves and is not a drift finding.
|
||||
6. **`## Out of scope`** — explicit non-goals, so agents stop expanding the diff.
|
||||
7. **`## Slicing plan`** — ordered, independently mergeable PRs (see below).
|
||||
8. **`## Drift log`** — append-only, agent-written.
|
||||
9. **`## PRs`** — append-only list of `#<n> — <state> — <ACs covered>`.
|
||||
|
||||
Optional: `## Open questions` (blocks `exploration.signoff`), `## Migration / compat` (mandatory
|
||||
whenever wire format, event schema, or a published API changes).
|
||||
|
||||
## Turning a spec into PRs
|
||||
|
||||
Run these steps in order. Do not skip step 2.
|
||||
|
||||
1. **Fetch and pin.** `git fetch origin` and record `origin/main`'s sha as `head`.
|
||||
Never start from the exploration branch, and never rebase it — the branch is
|
||||
reference material, not a base.
|
||||
2. **Drift check** against `base..head`:
|
||||
- Do all files, symbols and modules named in the spec still exist?
|
||||
- Have the invariants in `## Decisions` been contradicted by changes in main?
|
||||
- Do the commands in `## Acceptance criteria` still run?
|
||||
Write the result as a drift-log entry. On a contradicted decision: set `blocked`,
|
||||
do not implement.
|
||||
3. **Re-derive the slicing plan** if drift changed it. Each slice must:
|
||||
- build and pass CI on its own,
|
||||
- be reviewable in one sitting (target < 400 changed lines, hard stop at 800 —
|
||||
split further rather than exceeding it),
|
||||
- map to at least one acceptance criterion, and
|
||||
- land behind a flag or be inert if it is not the last slice of a user-visible
|
||||
change.
|
||||
4. **Implement one slice at a time**, branch `spec/<spec-slug>/<n>-<short>` off the
|
||||
pinned `head`. Tests for the slice's ACs go in the same PR as the code. A PR merges
|
||||
only when every check of every AC it lists passes on that PR: automated checks in
|
||||
CI, manual checks performed by a human reviewer with the result recorded in the PR
|
||||
description. An AC is *covered* once such a PR has merged.
|
||||
5. **Open the PR** linking the spec path and listing the ACs it covers. Update
|
||||
`## PRs` and `status`.
|
||||
6. **Do not touch merged or reviewed slices.** See the regeneration rule.
|
||||
|
||||
## The regeneration rule
|
||||
|
||||
Regeneration from spec is only allowed **before a human has reviewed the PR**.
|
||||
|
||||
Once review has started, the PR is a shared artifact: rebase, fix forward, push
|
||||
incremental commits. Regenerating discards the reviewer's context and silently
|
||||
invalidates their approvals. If regeneration is unavoidable after review, close the
|
||||
old PR with an explanation and open a new one — never force-push a fresh generation
|
||||
over a reviewed branch.
|
||||
|
||||
## Cross-repo and protocol work
|
||||
|
||||
When `repos` has more than one entry, PRs are opened in `repos` order and each PR
|
||||
body states which upstream PR it depends on. Merging out of order is a spec
|
||||
violation, not an optimisation.
|
||||
|
||||
When `specs` is non-empty:
|
||||
- the implementation PR references the section of the MSC it implements,
|
||||
- unmerged MSCs require the implementation to be feature-flagged and documented as
|
||||
unstable (unstable prefixes, not stable identifiers), and
|
||||
- any deviation from the MSC text is recorded in `## Drift log`, not silently
|
||||
resolved in favour of the code.
|
||||
|
||||
## Drift-log format
|
||||
|
||||
Append-only, newest last:
|
||||
|
||||
```
|
||||
### 2026-09-14 — drift check (base 4f2a1c9 → head 9b17e02)
|
||||
- `ReactionsReader.getLastReactionEvent` now takes a `CallMembership` instead of an
|
||||
event id → AC2 and AC3 check commands still valid, slice 3 needs rework
|
||||
- BLOCKING: decision D2 assumes a membership event change may keep a hand raised; main
|
||||
now clears all reaction relations on membership replacement. Needs owner decision.
|
||||
```
|
||||
|
||||
Drift entries are never rewritten or deleted, including entries that were later
|
||||
resolved. The log is the record of why the implementation diverged from the
|
||||
exploration.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- Never fill a `[NEEDS CLARIFICATION]` marker with a guess, in any state.
|
||||
- Never put file paths, symbols or library names in the product spec.
|
||||
- Never reference a file path or an internal symbol in `## Decisions` or
|
||||
`## Acceptance criteria`. Behaviour, protocol identifiers, concepts and test names
|
||||
only.
|
||||
- Never edit `<stem>.product.md` after `draft`, and never renumber an FR or SC id.
|
||||
- Never add a line that does not constrain the implementation, constrain its
|
||||
verification, or record a measured fact. It goes in the sidecar or nowhere.
|
||||
- Never modify `## Decisions`, `## Problem`, or `## Acceptance criteria`. These are
|
||||
the human contract. Propose changes in `## Open questions` and set `blocked`.
|
||||
- Never invent measurements or acceptance checks. Absent is better than plausible.
|
||||
- Never widen scope past `## Out of scope`, even when the fix looks trivial.
|
||||
- Never open a PR while `status` is `draft`, any `exploration.*`, or `blocked`.
|
||||
- Never delete a spec file. Terminal states are `in_production` or `abandoned`.
|
||||
- If the spec and current `main` disagree about how the system works, **main wins as
|
||||
description, the spec wins as intent** — record the difference and stop.
|
||||
|
||||
## Spec template
|
||||
|
||||
```markdown
|
||||
---
|
||||
status: draft
|
||||
blocked_from: null
|
||||
title:
|
||||
owner:
|
||||
signoff:
|
||||
eng: pending
|
||||
design: pending
|
||||
product: pending
|
||||
experiment_ref:
|
||||
base:
|
||||
repos: []
|
||||
specs: []
|
||||
supersedes: []
|
||||
superseded_by: null
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
## Decisions
|
||||
- D1 — <decision> — <why>
|
||||
|
||||
## Acceptance criteria
|
||||
- AC1 [FR-###] — <observable behaviour>
|
||||
- check: <command or exact manual steps>
|
||||
- AC2 [internal] — <invariant>
|
||||
- check: <command, anchored by test name>
|
||||
|
||||
## Rejected alternatives
|
||||
- <approach> — <why it failed> — <evidence>
|
||||
|
||||
## Measurements
|
||||
- <metric>: <value> (method: <command / setup>)
|
||||
|
||||
## Out of scope
|
||||
|
||||
## Slicing plan
|
||||
1. <slice> — covers AC1 — depends on: none
|
||||
|
||||
## Drift log
|
||||
|
||||
## PRs
|
||||
```
|
||||
@@ -0,0 +1,117 @@
|
||||
# Feature Specification: [FEATURE NAME]
|
||||
|
||||
**Feature Branch**: `[###-feature-name]`
|
||||
**Created**: [DATE]
|
||||
**Feature Spec**: `[YYYY-MM_Title.md]` <!-- status lives in the feature spec's frontmatter, not here -->
|
||||
**Owner**: [product handle]
|
||||
**Source**: [URL of the tracker/doc this mirrors, or "this file"]
|
||||
**Input**: User description: "$ARGUMENTS"
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
<!--
|
||||
IMPORTANT: User stories should be PRIORITIZED as user journeys ordered by importance.
|
||||
Each user story/journey must be INDEPENDENTLY TESTABLE - meaning if you implement just ONE of them,
|
||||
you should still have a viable MVP (Minimum Viable Product) that delivers value.
|
||||
|
||||
Assign priorities (P1, P2, P3, etc.) to each story, where P1 is the most critical.
|
||||
Think of each story as a standalone slice of functionality that can be:
|
||||
- Developed independently
|
||||
- Tested independently
|
||||
- Deployed independently
|
||||
- Demonstrated to users independently
|
||||
-->
|
||||
|
||||
### User Story 1 - [Brief Title] (Priority: P1)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently - e.g., "Can be fully tested by [specific action] and delivers [specific value]"]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
2. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - [Brief Title] (Priority: P2)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - [Brief Title] (Priority: P3)
|
||||
|
||||
[Describe this user journey in plain language]
|
||||
|
||||
**Why this priority**: [Explain the value and why it has this priority level]
|
||||
|
||||
**Independent Test**: [Describe how this can be tested independently]
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** [initial state], **When** [action], **Then** [expected outcome]
|
||||
|
||||
---
|
||||
|
||||
[Add more user stories as needed, each with an assigned priority]
|
||||
|
||||
### Edge Cases
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right edge cases.
|
||||
-->
|
||||
|
||||
- What happens when [boundary condition]?
|
||||
- How does system handle [error scenario]?
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: The content in this section represents placeholders.
|
||||
Fill them out with the right functional requirements.
|
||||
-->
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST [specific capability, e.g., "allow users to create accounts"]
|
||||
- **FR-002**: System MUST [specific capability, e.g., "validate email addresses"]
|
||||
- **FR-003**: Users MUST be able to [key interaction, e.g., "reset their password"]
|
||||
- **FR-004**: System MUST [data requirement, e.g., "persist user preferences"]
|
||||
- **FR-005**: System MUST [behavior, e.g., "log all security events"]
|
||||
|
||||
*Example of marking unclear requirements:*
|
||||
|
||||
- **FR-006**: System MUST authenticate users via [NEEDS CLARIFICATION: auth method not specified - email/password, SSO, OAuth?]
|
||||
- **FR-007**: System MUST retain user data for [NEEDS CLARIFICATION: retention period not specified]
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **[Entity 1]**: [What it represents, key attributes without implementation]
|
||||
- **[Entity 2]**: [What it represents, relationships to other entities]
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
<!--
|
||||
ACTION REQUIRED: Define measurable success criteria.
|
||||
These must be technology-agnostic and measurable.
|
||||
-->
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: [Measurable metric, e.g., "Users can complete account creation in under 2 minutes"]
|
||||
- **SC-002**: [Measurable metric, e.g., "System handles 1000 concurrent users without degradation"]
|
||||
- **SC-003**: [User satisfaction metric, e.g., "90% of users successfully complete primary task on first attempt"]
|
||||
- **SC-004**: [Business metric, e.g., "Reduce support tickets related to [X] by 50%"]
|
||||
@@ -9,3 +9,12 @@ This folder contains documentation for setup, usage, and development of Element
|
||||
- [MatrixRTC modes](./matrix_rtc_modes.md)
|
||||
- [Self-Hosting](./self_hosting.md)
|
||||
- [Developing with linked packages](./linking.md)
|
||||
|
||||
### Writing code for Element Call (agent-readable conventions)
|
||||
|
||||
Entry point: [AGENTS.md](../AGENTS.md).
|
||||
|
||||
- [Agent workflow](./agents/workflow.md)
|
||||
- [Architecture](./agents/architecture.md)
|
||||
- [Code style](./agents/code-style.md)
|
||||
- [Testing](./agents/testing.md)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Architecture
|
||||
|
||||
## Logic lives in view models; components render
|
||||
|
||||
- Call logic lives in `src/state/`. A component that derives call state is
|
||||
misplaced logic.
|
||||
- Composing behaviors: a factory taking `(scope, ...deps$)`, returning named
|
||||
behaviors plus callbacks. Copy `src/state/LayoutSwitchViewModel.ts`. Prefer this
|
||||
for anything new.
|
||||
- Owning a resource: a class taking the scope in its constructor — `MediaDevices`,
|
||||
`MuteStates`, `TileStore`, `Publisher`, `Connection`. Both styles are current;
|
||||
don't convert one to the other in passing.
|
||||
- `foo$` is an observable. `Behavior<T>`, an observable with a current value, is
|
||||
the default for anything a view reads.
|
||||
|
||||
## The view model / view contract
|
||||
|
||||
- Components take `vm: ViewModel<Snapshot>` and read state through `useBehavior`.
|
||||
See `CallFooter`, `InCallView`, `LobbyView`, `SettingsModal`.
|
||||
- A snapshot is `Actions & State`; every field becomes a `foo$` behavior, and none
|
||||
is optional.
|
||||
- An unavailable action is `undefined`, not a separate `canDoThing` flag — the
|
||||
presence of the callback drives the rendering.
|
||||
- Subscribe in an effect only to drive a side effect off an event stream, as
|
||||
`ReactionAudioRenderer` does; never to read state a behavior already holds.
|
||||
|
||||
## Scopes own lifetimes
|
||||
|
||||
- `ObservableScope` bounds every subscription a view model creates.
|
||||
- Reference only the scope defined in the same function; one captured from an
|
||||
enclosing scope outlives its owner, and `no-observablescope-leak` rejects it.
|
||||
|
||||
## Nothing reads the page
|
||||
|
||||
Element Call can be mounted several times inside a host's React tree, so it owns
|
||||
neither window, URL, document nor router. Each seam defaults to the old standalone
|
||||
and widget behaviour.
|
||||
|
||||
| Never | Use |
|
||||
| --------------------------------------------- | ------------------------------------------------------------- |
|
||||
| the `widget` global | `useHostBridge()` — `src/HostBridge.ts` |
|
||||
| `getUrlParams()`, `window.location` | `useUrlParams()` |
|
||||
| `useNavigate("/")`, `<Link to="/">` | `useLeaveToHome()`, `LeaveToHomeLink` |
|
||||
| `document.body` | `useRootElement()` |
|
||||
| `window.innerWidth/Height`, `useMediaQuery` | `useRootSizeMatches()` in views, `windowSize$` in view models |
|
||||
| global `i18next` | the instance in `src/utils/i18n.ts` |
|
||||
| config or analytics reading their environment | `Config.initWith()`, `PosthogAnalytics.configure()` |
|
||||
|
||||
- View models take values as options, never `getUrlParams()`
|
||||
(`callViewModelOptionsFromParams`, `CallViewModelOptions.hostBridge`).
|
||||
- The host bridge is the only channel to the host: `createWidgetHostBridge(widget)`,
|
||||
`nullHostBridge` standalone, `useComponentHostBridge`. State a capability
|
||||
(`supportsReactions`, `supportsProfileChanges`); never infer it from being a
|
||||
widget.
|
||||
- Shortcuts and portals attach to the root element, so two instances don't fight.
|
||||
- Known debt, not precedent: `Grid` measures `window.innerHeight`; `ErrorView`'s
|
||||
reload and `getAbsoluteRoomUrl` use `window.location`; recaptcha appends to
|
||||
`document.body` on the standalone login path.
|
||||
|
||||
## Context differences are options, not checks
|
||||
|
||||
- Named options with per-intent defaults (`configurationForIntent`), never a
|
||||
runtime check for who is hosting.
|
||||
- `controlledAudioDevices`, not the platform, is what makes `MediaDevices` pick
|
||||
`AndroidControlledAudioOutput` / `IOSControlledAudioOutput` over the web
|
||||
`AudioOutput`. Intent presets set it on non-desktop; standalone leaves it off.
|
||||
- URL params are a published contract: change one, update `docs/url_params.md`.
|
||||
|
||||
## Build targets
|
||||
|
||||
Code that builds in only one is a bug.
|
||||
|
||||
- `build:full` — standalone app, also widget mode.
|
||||
- `build:embedded` — `@element-hq/element-call-embedded`.
|
||||
- `build:sdk` — SDK library, entry `sdk/main.ts`.
|
||||
- `build:component` — `@element-hq/element-call-component`, sources in `component/`
|
||||
(its own pnpm project; run pnpm from the repo root). Host API is in the README;
|
||||
`pnpm lint:externals` rejects an import of a `react` / `react-dom` /
|
||||
`matrix-js-sdk` / `livekit-client` subpath the externals list omits.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Code style
|
||||
|
||||
_Clean Code_ (Robert C. Martin): small functions, intention-revealing names, low
|
||||
noise.
|
||||
|
||||
## Reuse before you build
|
||||
|
||||
Look before you write, in this order:
|
||||
|
||||
1. **Compound Web** — `@vector-im/compound-web` for buttons, tooltips, alerts,
|
||||
dialogs, menus, form controls, avatars; icons in
|
||||
`@vector-im/compound-design-tokens/assets/web/icons`.
|
||||
2. **This repo** — `src/button/`, `src/components/`, `src/input/`, `src/form/`,
|
||||
`src/tabs/`, and `Modal`, `Avatar`, `Toast`, `Slider`, `ErrorView`,
|
||||
`FullScreenView` at the root of `src/`.
|
||||
3. **The nearest existing feature** — grep for the same shape first. A hook is
|
||||
often what you want to extract, not a component.
|
||||
|
||||
If nothing fits, name the new shared component in the PR body and say what you
|
||||
rejected and why — never add one silently. Extend or parameterise before forking,
|
||||
and extract shared logic into a hook both callers use before copying it; if you
|
||||
copy, the PR says so.
|
||||
|
||||
## Ordering
|
||||
|
||||
- **Newspaper order.** Headline first, detail down. A file opens with what it
|
||||
exists to provide.
|
||||
- **Stepdown rule.** Caller above callee. A helper used by one function sits
|
||||
beneath it, which needs a `function` declaration, not a `const` arrow. Existing
|
||||
arrow components above their callers are not a pattern to extend — nor to churn.
|
||||
- **Suites.** Test cases first, helpers below. Older suites invert this; follow the
|
||||
rule in new ones, don't reorder old ones.
|
||||
- **Names.** What a thing means, not what it is made of: `naturalLayout$`, not
|
||||
`computedLayout$`. Comments explain why, never what.
|
||||
|
||||
## TypeScript, React and RxJS idiom win on a clash
|
||||
|
||||
- Hooks stay unconditional at the top of a component.
|
||||
- `useCallback` / `useMemo` dependency arrays sometimes force inlining.
|
||||
- An RxJS pipeline stays one expression. Name the behavior, not each operator.
|
||||
- A long factory is fine when it reads as a list of named behaviors.
|
||||
`CallViewModel.ts` is long because the domain is.
|
||||
- Marble test tables are dense on purpose.
|
||||
|
||||
## Enforced by lint
|
||||
|
||||
- Copyright header on every file: `Copyright <current year> Element Creations Ltd.`
|
||||
plus the AGPL / commercial SPDX line.
|
||||
- `logger` from `matrix-js-sdk/lib/logger`, never `console`, and no top-level
|
||||
`logger.getChild()`. The `console` ban is only enforced under `src/*/**`.
|
||||
- No floating or misused promises; async functions typed `Promise<T>`.
|
||||
- Inline type imports (`import { type Foo }`), so matrix-js-sdk stays lazily
|
||||
loadable.
|
||||
- Deep-import `matrix-js-sdk/lib/<module>` as the codebase does. Banned is the bare
|
||||
`matrix-js-sdk/lib`, `lib/index` and anything under `src/`.
|
||||
|
||||
## CSS
|
||||
|
||||
- Compound components and `--cpd-*` tokens in CSS modules. A hardcoded colour or px
|
||||
spacing is a design question, not a licence to inline a hex.
|
||||
- Size against the root, not the window: `@container element-call (…)` and
|
||||
`cqw` / `cqh`, never `@media (width)` or `vw` / `vh`. Media queries stay correct
|
||||
only in standalone-only views — home, login.
|
||||
- Style `[data-element-call-root]`, never `body` or `:root`; the component build
|
||||
makes those stand for the root (`component/build/scopeStylesToRoot.ts`).
|
||||
|
||||
## Strings and a11y
|
||||
|
||||
- Strings through `t()`: add the key, run `pnpm i18n`, fill `locales/en/app.json`.
|
||||
Other locales come from Localazy; never hand-edit them.
|
||||
- `t` from `useTranslation()`, or `src/utils/i18n.ts` outside React. Never the
|
||||
`i18next` global — several instances share a page.
|
||||
- Accessible names on controls, `aria-pressed` on toggles, keyboard reachability.
|
||||
`jsx-a11y` rules are errors; `vitest-axe` is available.
|
||||
@@ -0,0 +1,51 @@
|
||||
# Testing
|
||||
|
||||
Three layers; a user-facing feature needs all three. Unit proves the logic,
|
||||
Storybook proves the rendering, e2e proves the feature. Codecov gates 80% on the
|
||||
lines a PR touches.
|
||||
|
||||
## Unit
|
||||
|
||||
- View model tests use `withTestScheduler` + `testScope()` from `src/utils/test.ts`
|
||||
and read as ASCII timelines. Copy `LayoutSwitchViewModel.test.ts`.
|
||||
- Component tests use Testing Library, queried by role and accessible name.
|
||||
- Reuse the factories in `src/utils/test.ts` — `mockRemoteParticipant`,
|
||||
`mockMatrixRoom`, `mockLivekitRoom`, `MockRTCSession`. Hand-rolled mocks drift.
|
||||
- Snapshots live in `__snapshots__/`; update with `pnpm test <Name> -u`.
|
||||
- `component/**/*.test.ts` runs in the same jsdom project as `src`.
|
||||
|
||||
## Storybook
|
||||
|
||||
A deliverable, not documentation: `pnpm test:storybook` runs every story as a real
|
||||
browser test in the same CI job as the unit suite. **A UI change without a story is
|
||||
incomplete.** Only three stories exist so far, so copy `CallFooter.stories.tsx`.
|
||||
|
||||
- Drive the component from a snapshot via `useStaticViewModel`, never internals.
|
||||
- One named story per state that matters — loading, error, empty, denied, mobile.
|
||||
- Assert in a `play` function with `userEvent` / `expect` from `storybook/test`.
|
||||
- Mobile is `globals: { viewport: { value: "mobile2" } }`, not a second component.
|
||||
- Expose interesting props through `argTypes`.
|
||||
|
||||
`.storybook/preview.tsx` supplies `TooltipProvider`, `src/index.css` and English
|
||||
translations. The browser is Playwright's, so a fresh clone needs
|
||||
`pnpm playwright install` once.
|
||||
|
||||
## End-to-end
|
||||
|
||||
- `playwright/*.spec.ts` — standalone, on Chromium and Firefox.
|
||||
- `playwright/widget/` — widget mode, where cross-context bugs surface.
|
||||
- `playwright/component/` — the component in a host page, via the harness on port
|
||||
3001 that Playwright starts as a second web server. Catches container-relative
|
||||
layout, styles escaping the root, two instances on a page, host-bridge reports.
|
||||
- `playwright/mobile/` — Pixel 7, `mobile` project only.
|
||||
|
||||
Test what a user observes: the peer sees the change, it survives a reconnect, it is
|
||||
right after a reload. Reuse `playwright/spa-helpers.ts`,
|
||||
`playwright/widget/test-helpers.ts`, `playwright/fixtures/`.
|
||||
|
||||
```sh
|
||||
pnpm test # unit + storybook
|
||||
pnpm backend # Synapse + LiveKit, required for e2e
|
||||
pnpm test:playwright # or :open
|
||||
pnpm dev:component # component harness, port 3001
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
# Agent workflow
|
||||
|
||||
## Scratch files live in `agent-workspace/`
|
||||
|
||||
Git-ignored. One kebab-case subfolder per task, matching the branch topic:
|
||||
`agent-workspace/<slug>/`.
|
||||
|
||||
| File | Purpose |
|
||||
| --------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `plan.md` | The concrete approach — files, symbols, edit order — once direction is confirmed |
|
||||
| `commit-msg.txt` | `git commit -F agent-workspace/<slug>/commit-msg.txt` |
|
||||
| `pr-body.md` | `gh pr create --body-file agent-workspace/<slug>/pr-body.md` |
|
||||
| `implementation-summary.md` | What was built, decisions, trade-offs |
|
||||
| `NN-prompt.md` | Raw prompt text, numbered, when worth keeping |
|
||||
|
||||
- Nothing here is durable. The folder is ignored, so anything worth keeping is
|
||||
promoted into the PR body, a doc, or a feature spec before the task ends.
|
||||
- Never write scratch files to the repo root. Stray `load_test_summary.md` and
|
||||
`config.json_` files are what this folder prevents.
|
||||
- Start `pr-body.md` from `.github/PULL_REQUEST_TEMPLATE.md` and fill every
|
||||
section.
|
||||
- Commit subjects are plain imperative English. No conventional-commits prefixes.
|
||||
- A `plan.md` and a feature spec sit at different altitudes; neither replaces the
|
||||
other. A spec in `FEATURES_SPEC/` is durable and deliberately abstract —
|
||||
behaviour, decisions, acceptance criteria — so the feature can be rebuilt against
|
||||
a `main` nobody has seen yet. A plan is one slice of it landing on today's
|
||||
`main`: the paths, symbols and edit order the spec's decisions and criteria must
|
||||
not name. `FEATURES_SPEC/AGENTS.md` wins wherever the two genuinely overlap.
|
||||
|
||||
## Hand off before the quality pass
|
||||
|
||||
- Implement the change, run the narrowest check that rules out an obviously broken
|
||||
handoff, then stop and ask whether the direction is right.
|
||||
- Full `pnpm lint`, the whole suite, coverage and benchmarks come after the
|
||||
direction is confirmed.
|
||||
- Wider refactors, extra tests and documentation polish are follow-up work, not
|
||||
part of the first handoff.
|
||||
- If a check is needed before feedback, keep it to the touched code and say why.
|
||||
|
||||
## Commit and PR readiness
|
||||
|
||||
- Commit once the user confirms direction, or asks for one. Not before.
|
||||
- Before committing, every gate in [AGENTS.md](../../AGENTS.md#gates) is green and
|
||||
the change is covered at the layers [testing.md](./testing.md) asks for. Read the
|
||||
diff against [code-style.md](./code-style.md).
|
||||
- Re-run the whole checklist after any fix. Commit only on green.
|
||||
- Once a human has started reviewing, fix forward — never force-push a
|
||||
regeneration over a review in progress.
|
||||
Reference in New Issue
Block a user