277 lines · 13.8 KB · Markdown
RawDownload

Contributing to Lattice

Thanks for helping make Lattice a better research companion. Bug reports, feature ideas, documentation improvements, and focused code contributions are all welcome. Everyone taking part is expected to follow the Code of Conduct.

Report a bug or suggest an improvement

Before opening an issue, please search the existing issues to see whether it has already been reported.

For a bug, include:

  • the Lattice version and your macOS version;
  • clear steps to reproduce the problem;
  • what you expected and what happened instead;
  • relevant logs or screenshots, with private project content and credentials removed.

For a feature request, describe the research workflow you want to improve and the problem you currently encounter. Explaining the use case is more useful than prescribing a particular implementation.

Open an issue

Open a pull request

Small bug fixes and documentation improvements can go directly to a pull request. For a substantial feature, protocol change, or large refactor, please open an issue first so the direction can be discussed before implementation.

  1. Fork the repository and create a branch from main.
  2. Keep the change focused; avoid unrelated formatting or refactors.
  3. Add or update tests for behavior that changes.
  4. Run the relevant local checks described below.
  5. Open a pull request that explains the problem, the chosen solution, and how you verified it. Link the related issue when there is one.

Do not bump the application version in a normal pull request. Releases are prepared separately by maintainers; see docs/release-process.md.

Write the commit subject for a stranger. Lattice has no CHANGELOG.md — the release workflow builds each release's notes from the subjects of the commits in that tag's range, so your subject line is what users read. "Fix crash when opening a project with no root document" is a changelog entry; "fix bug" is not.

Changes that need extra care

Most of the codebase takes an ordinary pull request. One area does not, because there the tests can keep passing while the thing they protect breaks:

  • The Overleaf bridge (src-tauri/src/overleaf.rs + overleaf/, src-tauri/src/overleaf_rt.rs + overleaf_rt/, src/overleaf/). Lattice speaks Overleaf's undocumented browser protocol, including a hand-written Socket.IO 0.9 client — the legacy protocol, not the modern one every current library implements. UI and bug-fix work is fine, but a change to the wire format, the OT semantics, the permission model or persisted sync state has to arrive with a fixture that fails on the old behaviour and a rollback that does not discard local work. Read docs/overleaf-protocol.md first; it lists the specific "cleanups" that are not cleanups.

That area is not closed to contributions. It just wants an issue before a large change, so the compatibility question gets answered before the code exists.

Set up the project locally

You will need:

  • Node.js and pnpm — mise.toml pins the versions (Node 22, the major CI runs, and pnpm 10.13.1). Installing mise and running mise install in the repository root gets you both at the right versions, and pnpm check needs mise anyway.
  • stable Rust with rustfmt and clippy;
  • the desktop build prerequisites;
  • Git and a TeX distribution that provides latexmk.
corepack enable      # only if you are not using mise
pnpm install
node scripts/merge-po.mjs --install   # once per clone; worktrees share it

The last line registers the merge driver .gitattributes names for the Lingui catalogs (src/locales/*/messages.po). Without it, two branches that each add a string conflict on the generated catalogs; with it, git unions their entries and only stops when both sides translated the same message differently. Run pnpm i18n:check after such a merge: it re-extracts from the merged source, so a wrong union fails there rather than in CI.

The pre-commit file-size check

pnpm install also installs a pre-commit hook: its prepare script runs husky, which points this clone's core.hooksPath at .husky/_. A clone that has not run pnpm install has no hook. The hook runs lint-staged, which passes every staged file to scripts/check-file-size.mjs.

That check keeps each source file small enough for an agent to read in one pass: at most 8,000 tokens, counted as bytes / 4. Tests count too. A commit fails when it stages a source file over the budget, unless the file was already over the budget at HEAD and did not grow. The files that were over the budget when the check arrived may stay that size, but they cannot gain a byte, so split one before adding to it; the error message lists the split rules. node scripts/check-file-size.mjs --all prints every file over the budget, largest first.

Generated and vendored files are skipped through linguist-generated and linguist-vendored in .gitattributes, so mark a new one there rather than adding a skip list. A file whose every split would cost more than it saves, such as one table that only reads whole, can opt out with @ratchet-files-ignore in a comment within its first 10 lines, followed by the reason:

// @ratchet-files-ignore: one opcode table; split, each half reads without the other.

The check then ignores the file entirely, so use this only after looking for a seam, never to get past a refusal.

The local core.hooksPath that husky sets takes precedence over a global git config --global core.hooksPath, so the check runs whether or not you have one. The flip side is that hooks in your global directory stop running in this repository.

You probably do not need to build the bundled runtimes

This is the part that trips up most new contributors. Cargo compiles Tauri's resource manifest, which expects src-tauri/synara-runtime/ to exist, but the tests never launch the runtime. A stub file is enough:

mkdir -p src-tauri/synara-runtime
touch src-tauri/synara-runtime/placeholder.txt

With those files in place, pnpm check, cargo test, and cargo clippy all pass. Three of the four workflows that compile the Rust crate do exactly this — see the "Stub bundled runtime resources for compilation" step in .github/workflows/ci.yml, .github/workflows/flake-hunt.yml and .github/workflows/release-cache.yml, none of which ships a binary. .github/workflows/release.yml is the only one that stages the real pinned runtimes, because it is the only one that packages the app. src-tauri/synara-runtime/ is gitignored, so the stub stays out of your commits. src-tauri/presentation-runtime/ needs no stub: its placeholder.txt is tracked, and everything else in it is gitignored.

Unless you are changing the agent surface itself, stop here — you can run the whole gate, the frontend dev server (pnpm dev), and every test without the Synara source.

Running the full desktop app

pnpm tauri dev and pnpm tauri build do need the pinned Synara source, and they hard-fail with Synara source is missing at … when it is absent.

The location is not fixed: scripts/prepare-synara-sidecar.mjs reads SYNARA_SOURCE_DIR and otherwise falls back to sourceDirectory in scripts/synara-runtime.json, which moves whenever the pinned branch does. Derive everything from that file rather than hardcoding a path:

export SYNARA_SOURCE_DIR="$PWD/../synara"
git clone "$(node -p "require('./scripts/synara-runtime.json').repository")" "$SYNARA_SOURCE_DIR"
git -C "$SYNARA_SOURCE_DIR" checkout \
  "$(node -p "require('./scripts/synara-runtime.json').revision")"
# bun is a devDependency of this repo (package.json allowlists its install
# script for pnpm), so no global install is needed.
bun_bin="$PWD/node_modules/.bin/bun"
(cd "$SYNARA_SOURCE_DIR" && "$bun_bin" install --frozen-lockfile)

pnpm tauri dev

Keep SYNARA_SOURCE_DIR exported in the shell you run pnpm tauri dev from — Vite's .env.local is not visible to the build scripts. If you would rather not set it, clone into the path that node -p "require('./scripts/synara-runtime.json').sourceDirectory" prints instead.

scripts/setup-dev.sh automates all of this — mise and the pinned toolchain, Rust, the stub, and the Synara checkout — but it targets Debian/Ubuntu and will not run on macOS. It installs only into the repository, ~/.local and ~/.cargo, and prints the PATH line for you to add rather than editing your shell profile. Read it as executable documentation of the correct bootstrap order.

Licensed whiteboard builds also require VITE_TLDRAW_LICENSE_KEY in .env.local; see .env.example, which documents that and every other environment variable this repository reads — including the two credentials (OPENALEX_API_KEY, LATTICE_FIRECRAWL_KEY) that must be exported in your shell rather than put in .env.local, because Vite only loads .env.local for the frontend. If you add a new one, add it there in the same commit.

Verify your change

Run a focused test while iterating, then pnpm gate:changed before pushing:

pnpm vitest run src/path/to/file.test.ts
pnpm gate:changed

pnpm gate:changed (scripts/gate-changed.mjs) runs lint, typecheck, the i18n catalogs, and the tests that vitest related finds from what your branch changed against origin/main, plus the invariant tests no import leads to. It builds no Rust; CI's rust job covers a src-tauri/ change. docs/agent-validation.md has the details.

Vitest caches transformed modules in node_modules/.experimental-vitest-cache, so a repeat focused run skips most of its startup; pnpm vitest --clearCache empties it.

The full local gate, pnpm check, is mise run check and needs mise. It runs ten stages in parallel, skipping any whose declared sources have not changed since the last successful run:

Stage What it runs
i18n-check catalog extraction, drift check, compile, coverage guard
lint Oxlint over src/ and tools/, then knip (unused files, dependencies and exports) and the benchmark-table evidence check (pnpm perf:evidence)
test the Vitest suite
build typecheck plus the production web bundle, its WebKit built-in check and its size budget
literature-worker typecheck and test inside literature-worker/
open-slide-runtime the Open Slide presentation server's tests inside tools/open-slide-runtime/, then pnpm prepare:presentation, which stages the pruned runtime and fails unless it still serves the editor
cargo-fmt cargo fmt --check
cargo-test the Rust tests
clippy Clippy with warnings denied
notices THIRD_PARTY_NOTICES.md against the installed npm and crates trees

That covers literature-worker/ and tools/open-slide-runtime/ too, so there is nothing extra to run for a change in there. mise.toml is the definition; keep it and this table in step.

The commands are the same ones CI runs, except its interaction benchmark (pnpm perf:bench --check, or mise run perf-bench; see docs/performance.md), which needs a local Chrome, Playwright's WebKit and a few quiet minutes. The environments are not interchangeable either: CI runs everything from a clean checkout with nothing skipped (Rust on macOS, the rest on Ubuntu), while pnpm check skips stages it considers fresh. If a stage passes locally and fails in CI, suspect the freshness cache first — mise run --force check re-runs everything.

.no-mistakes.yaml configures the no-mistakes pipeline's test and lint commands and forbids its test step from launching the desktop app.

Work in the right area

Path What belongs there
src/ The desktop interface, editors, and frontend tests
src-tauri/src/ Project operations, LaTeX builds, Git, papers, Overleaf, and agent supervision
literature-worker/ The public literature proxy Worker and its tests
docs/ Design decisions and subsystem documentation for contributors

If you are unsure where a change belongs, open an issue with the intended user experience before investing in a large implementation.

Licensing

Lattice is licensed under the Apache License, Version 2.0 (see LICENSE and NOTICE). By opening a pull request you agree that your contribution is offered under those terms, as section 5 of the license describes. There is no CLA and no copyright assignment. Releases up to and including 0.1.341 were GPL-3.0-or-later and remain available under it.

The tldraw SDK (tldraw, @tldraw/editor, @tldraw/driver) is not covered by the Apache License; it stays under the tldraw license, whose verbatim text is in public/licenses/tldraw-LICENSE.md. Never commit a tldraw license key.

Trellis (@danfessler/trellis, @danfessler/trellis-react), the workspace's panel layout, is not covered by the Apache License either: it is licensed under its own non-commercial license (with a commercial tier), whose files are in public/licenses/trellis-LICENSE*.md. Keep those files and the attribution "Uses Trellis by DanFessler - github.com/DanFessler/trellis" (README, NOTICE and the macOS About panel). Change Trellis only through its pnpm patch, patches/@danfessler__trellis@0.2.0.patch.

When you add a dependency, adapt code from elsewhere, or bundle an asset, record it in THIRD_PARTY_NOTICES.md. That file also lists the attribution gaps that are known and still open; adding to that list is a perfectly good contribution.