Thanks for contributing. This document explains how to set up the repo, the quality bar every change must clear, and how to get a pull request merged.
Working with an AI coding agent?
AGENTS.mdis the machine-facing contract and is mandatory reading for agents. It enforces the same gates described here. Humans should read this file.
- Code of conduct
- Getting set up
- The quality gate
- Development workflow
- Fixing issues properly
- Commit conventions
- Pull requests
- Project-specific gotchas
- Reporting issues
Be respectful and assume good intent. Critique code, not people. Maintainers may remove comments, commits, and contributions that are abusive or off-topic.
Prerequisites
| Tool | Version | Notes |
|---|---|---|
| Bun | see packageManager in package.json |
Package manager and test runner |
| Node.js | >= 20 | Required by some tooling |
| Stellar CLI | 27.x | Only needed to regenerate contract bindings |
git clone https://github.com/SO4-Markets/interface.git
cd interface
bun install --frozen-lockfileUse --frozen-lockfile. It installs exactly what bun.lock pins and fails loudly
if package.json and the lockfile disagree, which is what CI does. A plain
bun install silently re-resolves versions and can hide dependency bugs from you
that CI will still catch. See Project-specific gotchas.
Every change must pass all of the following before you commit. These are the exact commands CI runs, in the same order. Run them from the repository root.
bun lint # ESLint across all packages — zero errors
bun typecheck # tsc --noEmit across all packages
bun run check:tokens # design-token policy (no raw hex / arbitrary sizes)
bun run test # unit tests
bun run test:coverage # unit tests + coverage thresholds
bun run build # production build of every packageIf you touched the indexer or the web app, also run the
Integration Checks steps:
# Indexer
bun run --cwd apps/s03-indexer codegen
bun run --cwd apps/s03-indexer build
bun run --cwd apps/s03-indexer test
SO4_CONTRACTS_REPO="$PWD/apps/s03-indexer/tests/fixtures/contracts-repo" \
bun run --cwd apps/s03-indexer sync:contracts:local
bash scripts/validate-manifest.sh apps/s03-indexer/config/contracts.local.json
# Web
bun run --cwd apps/web typecheck
bun run --cwd apps/web buildEnd-to-end tests are not part of the push CI, but run them for UI changes:
bunx playwright install --with-deps # first time only
bun run test:e2e- A red gate is never "unrelated". If
mainis broken, fix it or say so explicitly in your PR. Do not layer changes onto a failing baseline. - Never silence a check to make it pass. No
eslint-disable,@ts-ignore,@ts-expect-error,skip/onlyon tests, lowered coverage thresholds, or newcheck:tokensallowlist entries purely to get green. Each of these is a legitimate tool with a legitimate use — the rule is that the justification must be the code, not the pipeline. If you use one, explain why in a comment and in the PR. - Warnings are not errors, but don't add them. Lint currently passes with a small number of pre-existing warnings. Don't grow that number.
packages/contracts enforces: 85% lines, 85% branches, 80%
statements, 65% functions. Coverage reports land in each package's
coverage/ directory. If your change drops coverage below the gate, add tests —
do not lower the threshold.
Branch off main with a descriptive name:
git checkout -b feat/order-book-component
git checkout -b fix/chart-theme-flash
git checkout -b chore/upgrade-tanstack-query- Follow the existing feature-module structure (
components/,hooks/,lib/,data/). - One responsibility per file; keep components small.
- Prefer workspace imports (
@workspace/ui/...) over deep relative paths. - Use design tokens, not raw values —
check:tokensenforces this. Text sizes and radii have named tokens inpackages/ui/src/styles/globals.css. - Comment non-obvious intent only. Don't restate the code.
bun formatSee The quality gate. All of it, not a subset.
If you changed any package.json, bun.lock, tsconfig.json, or build config,
your local node_modules may not represent what CI installs. Verify in a clean
clone:
git clone file://$PWD /tmp/so4-cleanroom
cd /tmp/so4-cleanroom
bun install --frozen-lockfile
bun lint && bun typecheck && bun run test && bun run buildThis is not paranoia — it is how a whole class of "passes locally, fails in CI" bugs in this repo were found. See Project-specific gotchas.
When you pick up an issue, the expectation is a complete fix, not a patch that makes the symptom disappear.
- Reproduce it first. If you cannot reproduce it, say so in the issue rather than guessing at a fix.
- Find the root cause. Ask why the bad state was reachable at all. A
nullcheck that hides a value that should never have beennullis not a fix. - Fix the cause, not the symptom. If the same bug class exists elsewhere in the codebase, fix those too or file a follow-up issue naming them.
- Add a regression test that fails before your change and passes after. If a change genuinely cannot be tested, explain why in the PR.
- Address the whole issue. If you can only complete part of it, say exactly what is left and why — do not quietly narrow the scope.
- Leave the campsite clean. No commented-out code, stray debug logging, or
TODOwithout an issue link.
Scope discipline matters as much as completeness: fix the issue you picked up, and open separate PRs for unrelated cleanups you spot along the way.
We follow Conventional Commits:
feat: add limit order confirmation dialog
fix: resolve chart flicker on theme toggle
fix(ci): pin stellar-cli action to a release tag
chore: upgrade lightweight-charts to 5.3
docs: document contract integration stubs
refactor: extract oracle normalisation into shared util
test: cover referral code storage helpers
Types: feat, fix, chore, docs, refactor, test, perf, build, ci.
Write the body to explain why, not what — the diff already shows what. For non-obvious fixes, state the root cause and how you verified it.
Keep commits logically scoped. Unrelated changes belong in separate commits.
Open your PR against main with:
- What changed and why.
- How you verified it — which gate commands you ran, and their result.
- Screenshots or recordings for any UI change.
- Linked issue (
Closes #123). - Notes on contract-integration assumptions, if relevant.
A PR is ready for review when:
- Every command in The quality gate passes locally.
- CI is green on the PR.
- New behaviour has tests.
- No check was disabled, skipped, or weakened to achieve green.
- Commits follow Conventional Commits.
Maintainers may ask for changes. Push follow-up commits rather than force-pushing over review history, unless asked to rebase.
These have each caused a real CI failure. Read them before debugging one.
Bun's isolated linker does not hoist every transitive package to the root
node_modules. If a file imports a package, that package must be declared in
that workspace's own package.json.
A stale local node_modules can have leftovers that make an undeclared import
resolve on your machine and fail in CI. Two consequences worth knowing:
- A
declare module "x"augmentation whose specifier does not resolve is silently ignored — TypeScript creates a new ambient module instead of merging, and the types you expected simply are not there. - Hardcoded paths like
../../node_modules/<pkg>in atsconfig.jsonwill not resolve. Reference the package-local./node_modules/<pkg>instead.
apps/s03-indexer/src/typesis gitignored and produced bybun run --cwd apps/s03-indexer codegen. Any script that compiles the indexer must run codegen first, or it will fail on a fresh checkout.packages/contracts/src/generatedis checked in but carries hand-written adaptations (camelCase fields, extra*Args/*Valhelpers) thatsrc/clientsimports. Runningbun run contracts:gen:alloverwrites them and breaks typecheck. Seepackages/contracts/contracts.json.
This protocol is open source and contract IDs are public on-chain identifiers.
They live in packages/contracts/contracts.json. Regenerating bindings needs no
secrets:
bun run contracts:gen:all # all bindings
bun run contracts:gen:all exchange-router # just oneWeb tests run with MSW and onUnhandledRequest: "error". Every request a test
makes needs an explicit handler — add shared ones in
apps/web/test/msw/handlers.ts, or per-test with server.use(...). Tests must
never depend on real network calls.
Open a GitHub issue with:
- A clear title and description.
- Steps to reproduce, for bugs.
- Expected vs actual behaviour.
- Browser / OS / Bun version, if relevant.
- Relevant logs — the failing command's output, not a screenshot of it.
For suspected security vulnerabilities, do not open a public issue. Contact the maintainers privately.