Welcome. This file covers what is specific to this repository; general open-source etiquette is assumed.
| Branch | What it is |
|---|---|
main |
Stable, matches the released version. Only maintainers merge into it, from dev |
dev |
Integration branch. Every contribution lands here first |
Your path as a contributor:
git switch dev && git pull
git switch -c fix/some-thing # branch off dev, not main
# make changes, commit with -s (see DCO below)
git push -u origin fix/some-thingThen open a PR with base dev, not main. A maintainer merges once CI and review pass.
Merging dev → main is done by maintainers on their own schedule; contributors don't need to think about it. Two rules keep the two branches from drifting apart, and both are for maintainers:
- Back-merge
mainintodevright after a release. Thedev → mainmerge commit lives only onmain, so without thismainreads as ahead even though the trees are identical, and the gap grows by one every release. - Urgent fixes go through
devtoo. A PR opened straight againstmainis the one thing that makes the two branches genuinely diverge, and then someone has to reconcile them by hand. - A release moves the compose pin.
docker-compose.ymldefaultsUTOPIA_IMAGEto a fixed tag, and that tag is what the README's one-command start actually runs. Cutting a release is: mergedev → main, tag and publish, then a PR ondevthat points the pin at the new tag. rc6 and rc7 shipped with the pin still on rc5, so everyone who followed the README ran a three-week-old build. Tags from rc8 on are multi-arch; until the pin reaches one of them, arm64 hosts cannot pull the default image.
Both branches are protected: pull request required, CI (backend and web) must pass, no force pushes, no deletions, and admins are held to the same rules.
| Change | What to do |
|---|---|
| Bug fixes, docs, i18n strings, tests | Open a PR directly |
| New features, dependency changes | Open an issue first and describe the use case |
| Data model, ontology contract, public API | Discuss in an issue, then land an ADR before writing code |
docs/decisions/ is where this project's reasoning lives. An ADR records why this and not that, including the approaches that were tried and failed. For a change of any size, that document outlives the code.
Requires Docker, Rust 1.85+, Node 20+, pnpm.
PDF text extraction also uses pdftotext when the Rust parser cannot read a file. Source installs need Poppler and its CJK CMap data (poppler-utils poppler-data on Debian); the Docker image includes both. Without them the PDF fallback test skips, and a PDF that draws text we cannot read reports a reader failure rather than claiming the file is a scan.
docker compose up -d db # Postgres with pgvector
cargo run -p utopia-server # runs migrations, :1516
cd web && pnpm install && pnpm dev # :5173, proxies /api to the backendCI runs exactly this. Green locally means green in CI:
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cd web && pnpm install --frozen-lockfile && pnpm build # build type-checksThis is the easiest thing to get wrong here. A green cargo test --workspace does not mean everything ran. A number of tests begin like this:
let Some(url) = utopia_store::test_db::url() else {
return Ok(());
};test_db::url() reads UTOPIA_DATABASE_URL. With UTOPIA_TEST_REQUIRE_DB=1 also set, a missing database is a failure rather than a skip — that is how the migrations job in CI runs the whole utopia-store suite, so a green run there means the SQL was exercised. Use the same guard in new tests; do not read the env var directly.
They guard what the compiler cannot see: table aliases inside SQL strings, how NULL behaves in a comparison, rows an INNER JOIN silently drops, whether a recursive CTE expands the same ancestor twice under diamond inheritance. cargo check and clippy say nothing about any of it.
If you touched SQL under crates/utopia-store/, set it and run again:
# 1517 is the host-side port: docker-compose.yml deliberately avoids 5432
# so a locally installed Postgres does not collide with the dev container.
# Inside the compose network the app still talks to db:5432; 1517 is only
# for code on the host reaching the container.
export UTOPIA_DATABASE_URL=postgres://utopia:utopia@localhost:1517/utopia
cargo test --workspacehuman_phrase_materialization_delivery exercises the real store and kills child
processes at three commit boundaries. It starts the actual queue worker, so run
it only against a dedicated, otherwise idle test database, separately from the
workspace suite. Busy-lock coverage calls the private materialization body from
cfg(test) and observes the existing production entry point waiting on the lock.
These tests do not register an asynchronous production handler.
export UTOPIA_DATABASE_URL=postgres://.../dedicated_delivery_tests
export UTOPIA_TEST_REQUIRE_DB=1
cargo test --locked -p utopia-store --test human_phrase_materialization_delivery -- --ignored --skip crash_child --test-threads=1 --nocapture
cargo test --locked -p utopia-store --lib materialize::delivery_tests::busy_defers_without_retaining_connections -- --ignored --test-threads=1 --nocaptureThe first command explicitly runs both parents; the process-exit parent invokes
crash_child itself and kills and waits for each child. Do not run that child by
hand. See 0051
for the proposed delivery contract and remaining production acceptance.
Don't collide migration numbers. migrations/ rolls forward by number. Check the latest number on main before opening a PR — two branches each writing an 0011_ has happened, and after the merge neither one runs.
UI strings go in i18n. Add to both web/src/i18n/en.ts and zh.ts; no hard-coded strings in components.
Comments explain why. This repository comments densely and deliberately records the traps it fell into ("the first version used OR, and the Elon Musk article then produced a snapshot every 6KB"). Follow that. A comment restating what the code does will be asked to go.
Commit messages: one English sentence, stating the motivation. No long body. Skim git log for the register.
Every workflow declares its own permissions:. The repository default is now read-and-write, because one workflow commits a generated chart. A workflow without a permissions: block inherits that default, so leaving it out silently hands write access to something that only needed to read. Declare the minimum the job actually needs — contents: read for anything that just builds or tests.
We use the DCO, not a CLA. You keep the copyright on your code; you are certifying that you have the right to submit it under Apache-2.0.
Commit with -s and git adds the line for you:
git commit -s -m "Fix the thing"which appends:
Signed-off-by: Your Name <your@email>
Forgot? git commit --amend -s for the last commit, or git rebase --signoff HEAD~3 for several (adjust the count), then git push -f.
Use a consistent identity you answer to — a GitHub account with history under it counts — and an address that reaches you; a noreply address tied to that account is fine. No anonymous or throwaway contributions.
By contributing you agree that your work is released under Apache-2.0.