Skip to content

Contributing

This section documents the standard development workflow and quality bar for SilentSwarm changes.

Current topics

  • Environment setup uses Python 3.11+ and editable installs from the repository root.
  • Docstring style for public modules, classes, and functions.
  • UI code structure: page code as ES modules, and the rules a page follows.
  • Code style expectations: keep changes focused, prefer existing package patterns, and avoid generated artifact churn.
  • Testing expectations: add or update unit tests for new behavior; add integration coverage when behavior crosses leader/fellow/CLI boundaries.
  • Documentation expectations: update the closest README or docs page whenever user-facing behavior, commands, config, or APIs change.

Setup

python3 -m venv .venv
source .venv/bin/activate
pip install -U pip
pip install -e ".[dev,docs]"
python3 scripts/vendor_frontend.py     # pinned UI libraries into the leader's templates/

Add ,runtime to the extras only on a GPU machine — that pulls PyTorch. The leader is deliberately torch-free and must stay importable without it.

Repository layout

src/silent_swarm/
  leader/        control plane (FastAPI + web UI) - see below
  fellow/        the GPU worker: runner, heartbeat, signals, inventory, log shipper
  cli/           the `swarm` CLI, one module per command group
  bootstrap/     the fellow bootstrap entry point
  shared/        contracts.py (leader/fellow/CLI wire types), partitioning.py, mesh.py
  runtime/       model execution: loading, training loops, serving, export
  pipe/          the pipeline mechanic: split/ (L1), wire/ (L2), link/ (L3);
                 split/plan.py is the one place layer ranges are computed,
                 link/ the one transport between stages

The packages form three layers, and imports only point down:

Layer Packages Holds
Application leader, fellow, cli, bootstrap, shared, config users, pools, jobs, placement policy, UI, worker lifecycle. Torch-free.
Runtime runtime (and data, model) what a GPU worker does with the mechanic: load a model, train a stage, generate, export. torch only in runtime/torch/.
Mechanic pipe splitting a model (split/), framing tensors (wire/), moving bytes (link/). Imports nothing else from silent_swarm; torch only in pipe/split/torch/.

pipe/ follows the module layout and contracts of the standalone swarmpipe library, so code can move between the two by renaming the package. The application packages do not import each other, and the fellow starts the runtime as a subprocess rather than importing it. tests/unit/test_layering.py checks every import against these rules. Code from before the split still breaks a few of them; the test lists each case, and that list may only get shorter.

The leader is the largest package, so it is grouped by domain rather than left flat. app.py (the FastAPI app and its lifespan) and route_policy.py (the authorization map covering every route) sit at the top; everything else lives in the subpackage that owns it:

Package Owns
accounts/ users, auth, admin user management, API tokens, the audit log
pools/ the pool store and its HTTP surface
buckets/ pool-scoped training sets and their HTTP surface
cluster/ node lifecycle: bootstrap contract, join sessions, worker registry, signal hub, log buffers
telemetry/ health windows, serving metrics, grades, metric history, VRAM budget, node replacement
notify/ transactional email and operational alerts
training/ jobs, job dispatch (placement, dtype, transport, START), checkpoints, job metrics
relay/ the HTTP relay for pipeline frames and who may use a lane
serving/ servable-model export, catalog, inference broker, serving placement
web/ page builders and templates/ (hand-written HTML/CSS/JS, no framework)

tests/unit/<area>/ mirrors this tree, subpackages included — a new test for leader/telemetry/grades.py belongs in tests/unit/leader/telemetry/. Tests that cut across the whole app (the route-policy sweep, CSRF enforcement, the relay) sit at tests/unit/leader/, next to what they exercise in app.py.

Imports inside the leader are always absolute (from silent_swarm.leader.pools.store import PoolStore); nothing is re-exported from an __init__.py, so every import names the module it comes from.

Validation

Everything CI runs, in the order it runs it:

python -m ruff check .                  # lint
python -m pytest tests/unit -q          # fast suite, no cluster needed
python -m pytest tests/integration -q   # leader/fellow/CLI boundaries
mkdocs build --strict                   # docs must build clean

A new import that crosses a layer the wrong way fails tests/unit/test_layering.py; move the code to the right layer rather than adding it to the test's list of known violations.

A new leader API route also has to be classified in leader/route_policy.py — unlisted routes are default-deny, and tests/unit/leader/test_route_policy.py fails until you add them.

Changes that touch the web UI run the frontend job as well:

biome ci .                                   # lints the templates JS
npx typescript@5.7.2 tsc -p jsconfig.json    # // @ts-check + JSDoc, no build step
node --test tests/js/*.test.mjs              # the pure shared/ss modules, no browser
node --test tests/ui/*.test.mjs              # behaviour tests in a browser (~1 min)
node scripts/frontend_smoke.mjs              # boots the leader, loads every page

The smoke test seeds a pool with a heartbeating node, a bucket and a site banner — sending the CSRF header the browser would, and stopping on any seed that does not answer 2xx, so no page silently drops out of the run. It then fails on any console error, failed request, request to a non-vendored third-party host, serious/critical axe-core violation (colour contrast included), or one of the layout regressions it checks by measuring the page: console fonts and sidebar chips, clipped inference cards, node-table cells, the bucket's pool name, the mobile drawer toggle against the banner and logo, the new-bucket form at 320 px, and wrapped profile labels. A broken page or a regressed contrast ratio fails the build rather than reaching a screenshot.

To scan the docs as well, build them and point the smoke test at the output:

mkdocs build -d /tmp/docs-site
SMOKE_DOCS_DIR=/tmp/docs-site node scripts/frontend_smoke.mjs

Locally the docs findings print as warnings (warn); SMOKE_DOCS_STRICT=1 turns them into failures, which is how CI runs it.

Behaviour tests

tests/ui/*.test.mjs state what a user can do, one promise per test, and check it in headless Chrome: "a new bucket is created from a dialog on the table toolbar", "a pool page is tabbed, and each tab is an address of its own", "a new token is shown once". They run on Node's own test runner with the puppeteer-core the smoke test installs, so they need no other package. Each file starts its own leader on a throwaway database through tests/ui/fixture.mjs.

A test about behaviour belongs here, not in a Python test that reads the template's JavaScript: those break when code moves and say nothing about what the user sees. The Python suite keeps what the server owns - the shells, the CSP, the tokens and contrast, the markup rules in test_accessibility.py.

The page smoke test, the snapshots, the keyboard walkthrough and these tests share one harness, scripts/lib/leader_harness.mjs: start a leader, register the admin, seed the way the browser does (cookies plus the CSRF header, failing on any non-2xx), add a heartbeating node, open a signed-in page.

tests/unit/leader/web/test_ui_modules.py holds the rules the move to ES modules left behind (docs/planning/ui-modules-epics.md): no inline script in a template but the few listed pre-paint ones, every shared helper defined once in shared/ss/, no markup written as a string (innerHTML and the like; lit-html's unsafeHTML only for the chat's sanitised markdown), // @ts-check in every module, and no window.SS - page code imports what it uses, and shared/chrome.js is the first module on every page. Nothing is grandfathered: a page that breaks one fails the build.

Before/after snapshots for UI changes

scripts/ui_snapshots.mjs boots a seeded leader (two pools, three heartbeating fellows, a bucket, a pending account, a banner) and screenshots every page at desktop and phone size in each theme, plus the interactive states (menus, dialogs, toasts, the mobile drawer), with a full axe-core scan of each page in a11y.json and a contact sheet in index.html:

node scripts/ui_snapshots.mjs /tmp/before                 # on main
node scripts/ui_snapshots.mjs /tmp/after                  # on your branch
node scripts/ui_snapshots.mjs /tmp/after --themes light --pages app,states,docs \
  --docs-url http://127.0.0.1:3000                        # docs from a running frontend

It is not a CI gate - pixel diffs on live data are flaky - but attach both folders (or their index.html) to a pull request that changes the UI.

Documentation screenshots

The User Guide carries a screenshot of every page. Regenerate them whenever the UI changes:

# a throwaway leader with its own state, models root, and cookies over http
SWARM_JOIN_STATE_DB=/tmp/guide.sqlite3 \
SWARM_SERVABLE_MODELS_ROOT=/tmp/guide-models \
SWARM_COOKIE_SECURE=0 \
  python -m uvicorn silent_swarm.leader.app:app --port 8099

python scripts/screenshot_docs.py --base http://127.0.0.1:8099 \
    --state-db /tmp/guide.sqlite3 --models-root /tmp/guide-models \
    --out docs/06_user_guide/img

The script seeds a small but complete cluster — four heartbeating nodes, a deployed two-stage model, an hour of metric history — because a page with no data documents nothing. Needs playwright and a system Chromium.

  • tests/
  • src/
  • docs/
  • exp/

Documentation conventions

Moved here from the documentation home, which is for readers rather than authors:

  • Document current behavior first.
  • Keep navigation task-oriented and shallow.
  • Prefer Mermaid diagrams for system flows and state machines.
  • Keep historical planning separate from operator and developer docs.
  • Keep links relative, and give a link the page's title as its text - a bare some-file.md reads as a directory listing, not as documentation.
  • Verify with python -m mkdocs build --strict.

Building the docs locally

source .venv/bin/activate
pip install -e ".[docs]"
python -m mkdocs serve                 # live reload on http://127.0.0.1:8000
python -m mkdocs build --strict        # what CI runs

A standalone mkdocs serve has no /vendor/ path, so the brand fonts fall back to the system stack - that is expected, and the deployed site serves them from the same origin as the app. See The public web surface.

The docs theme

The docs use the app's design tokens, not a palette of their own. docs/assets/tokens.css is generated from the app's leader/web/templates/shared/theme.css and checked in; after changing a token in the app, regenerate it (a unit test fails until you do):

python scripts/docs_tokens.py           # rewrite docs/assets/tokens.css
python scripts/docs_tokens.py --check   # what tests/unit/scripts/test_docs_tokens.py asserts

It maps the app's dark theme onto Material's slate scheme and the light theme onto default, and Material's variables (--md-default-bg-color, --md-typeset-a-color, ...) onto the app's semantic tokens. docs/assets/extra.css then styles the docs with those tokens only - no raw colours, nothing below 12 px (tests/unit/docs/test_docs_theme.py).

The palette offers the app's three themes in the app's order - dark (the default for a first visit), light, system - and shares the choice with the app through the swarm_theme cookie, in both directions. The header, the search dialog, the breadcrumbs, the tabs and the 404 page are Material partials overridden in overrides/, each with a comment saying what differs from Material's own; they are written against mkdocs-material 9.7, the floor the frontend image pins.