The operational “how to work in this repo” guide, for coding agents (Claude Code
auto-loads CLAUDE.md, which points here; Codex and others read this file
directly) and human contributors alike. Humans wanting the narrative — why
prima-bench exists, and the consumer + developer walkthroughs — should read
GUIDE.md, the human guide; this file is its working companion.
prima-bench is a benchmarking library for biological AI models: model
predictions in (EvalData), scores out (EvalResult). It never imports a
model framework and never runs or trains models.
Read before you change anything
- ARCHITECTURE.md — the directory map, layer separation,
invariants, and coordinate conventions. Read it before making or planning
changes, and don’t make changes that alter the decisions recorded there. If a
request can’t be met within the current architecture, update
ARCHITECTURE.mdand the relevantREADME.mdin the same PR. - The per-layer
src/prima_bench/<layer>/README.mdfor whatever you’re touching, and GUIDE.md Part 3 for the full add-a-task/metric/dataset/recipe walkthrough.
Where things live (edit in the right layer)
| Change | Goes in |
|---|---|
A value type (EvalData, EvalResult, Contract) |
core/ |
The Metric base class, device / stats helpers |
metrics/ |
| A cross-task numerical helper (kNN, summary) | transforms/ |
| Register / look up by name; split-preparer keys | registry/ |
| Hashing, cache, downloads, derived-file cache, tabular reads | io/ |
| A metric, task, contract, or split preparer | tasks/<task>/ (see tasks/README.md) |
A dataset descriptor (name@version → artifact URIs) |
datasets/<dataset>/ |
VEP sequence access (internal) / pb.get_sequences |
genome/ / sequences.py |
The pb.Evaluation entry point |
runner/ |
| Curation that writes bucket bytes | recipes/ (separate package — see recipes/README.md) |
Invariants you must not break
Full detail is in ARCHITECTURE.md; this is the guardrail checklist.
solutionis never reachable from consumer code — nofetch_*returns it;pb.Evaluationloads it internally.pb.fetch_datareturns(train_path, test_path)— no per-task polymorphism; test-only datasets expose notrainsplit and raiseKeyError(usepb.fetch_test).- Censorship is by
NaN, never zero or absence. - Versioned bytes are immutable — any byte change requires a new
v{version}/. - Metrics are dual-mode — state holds raw inputs;
computedoes all the math, so single-process and sharded/distributed runs give identical results. - Coordinates: a variant
posis 1-based; an index / slice is 0-based half-open. Off-by-ones here are silent correctness bugs.
General Principles
- State assumptions clearly and explicitly
- Write well-designed, modular, extensible code
- Avoid writing code or plans that would change decisions in
ARCHITECTURE.md. If user requests cannot be satisfied with the current architecture, ensure thatARCHITECTURE.mdand correspondingREADME.mdfiles are updated to reflect changes.
Code reuse, DRY & conciseness
- Search before you write. Before adding any function, helper, class, or constant, grep/glob for something that already does it. Check
core/(EvalData,Contract, results),metrics/(theMetricbase class),io/(download/cache, hashing), and theregistrydecorators (register_metric/register_task/register_dataset) first. Reuse existing bases and registered components rather than re-deriving them; don’t hand-roll a metrictorchmetricsalready provides. - Reuse / extend / create. Existing code covers ≥~80% of the need → reuse it (import; never copy-paste). Close but not exact → extend it without breaking callers. Write new only when nothing fits — with a one-line note on why. Rule of three: the third duplicate gets factored into a shared helper; define constants once (
UPPER_SNAKE), no repeated magic values. - Be concise. Minimum code that solves the problem: no speculative generality, unused params, or defensive scaffolding for impossible cases. Match the surrounding file’s density; skip docstrings on obvious helpers; prefer composition / expression chains over long imperative blocks. Keep comments concise too — explain why when it’s non-obvious, not what the code already says, and skip full background-context dumps.
Logging & output
- Diagnostics go through
logging, notprint. Use a module logger (logger = logging.getLogger(__name__)) for progress, warnings, and errors; calllogging.basicConfig(...)once at the CLI entry point. These land on stderr. - Reserve
print()(stdout) for the tool’s actual data product — e.g. a report table a caller may pipe or parse. Never mix diagnostics into stdout. - Recipe helpers
raise, they don’tsys.exit. In recipe code (recipes/**/recipe.pyand the shared modules —utils.py,storage.py,obo.py,qtl_*.py, …), a validation/precondition failure inside a helper function mustraisean exception (ValueError,FileNotFoundError, …), notsys.exit(...). Helpers are imported bymain(), byexplore.pypreflights, and by unit tests — asys.exitthere can’t be caught orpytest.raises-tested and tears down the whole process. Only the script boundary owns the exit code: amain(),explore.pypreflight control flow, and the sharedutils.fail_if_mismatchesverdict maysys.exit. A guard test (recipes/tests/test_no_sysexit_in_helpers.py) enforces this in CI. - Joining
gs://paths:os.path.join(base, *parts)works fine forgs://URIs — as long asbasecarries the scheme (don’t pass a bare"gs://"as its own component). No dedicated helper needed.
Data licensing (read before adding any dataset)
Every dataset curated into prima-bench must be released under a license that permits commercial use, redistribution, and derivatives — we run commercial models against these benchmarks. “Academic / non-commercial research only” data cannot enter the repo.
- The acceptable / not-acceptable classification is
recipes/LICENSING.md— the single source of truth. - Licensing maintainers: Francisco M. Martín-Zamora (
francisco@primamente.com) and Pouya Niki (pouya@primamente.com). All licensing questions and approvals go through them. - Before writing a new
recipes/{publication}/{dataset}/recipe, confirm the upstream license is on the Acceptable list. If it isn’t clearly acceptable, stop and ask the user before proceeding — re-curating around a bad license later is expensive. - Each dataset README must carry a
## Licensesection with an SPDX token — or an approved fallback (public-domain/US-Gov-PD) where no SPDX identifier exists (see the per-dataset template inrecipes/README.md). - Flag this to the user whenever a PR adds or changes a dataset/recipe and the license is missing, non-commercial, no-derivatives, controlled-access, or unclear — don’t let it slip through silently. A not-acceptable license is not an automatic no: tell the developer to raise it with the maintainers (Francisco / Pouya) to evaluate the risk and the exact use case, since testing is sometimes allowed. CI (
.github/workflows/license-check.yml) enforces the same gate, but call it out at authoring time.
External systems
- Linear: issues for this repo live in the prima-bench project on the Tech team (
https://linear.app/prima-mente/project/prima-bench-8b623afc8174). File new tickets there — not against the Tech team root, and not in any other project.
Worktrees
Before making code changes, create a new worktree for the task. worktrees live under .worktrees/.
Testing
-
CI (
.github/workflows/ci.yml) gates a PR on four things, so run them locally before pushing:uv run ruff check src tests,uv run pyright,uv run pytest --cov=prima_bench, and an import-smoke of every recipe (uv run --project recipes pytest recipes/tests/ -q). -
The standard suite runs with
uv run python -m pytest. -
Some integration tests are skip-by-default: they cross-check our code against an external reference library that is not a project dependency, so they
importorskipand are silently skipped in CI. Run them by overlaying the dependency, e.g. the indel/sequence cross-check against kipoiseq:uv run --with kipoiseq python -m pytest tests/integration/test_indel_matches_kipoiseq.pyRe-run the relevant differential test whenever you touch the code it validates (the source it covers carries a
NOTEpointing back here).
Adding things — quick index
- A metric on an existing task → drop a file in
tasks/<task>/metrics/, decorate it with@register_metric, import it from that dir’s__init__.py, and add its name to the task’smetricstuple. (tasks/README.md→ “Adding a metric”.) - A new task →
tasks/README.md→ “Writing a new task”: contract → metrics → split preparers →register_task. - A new dataset →
datasets/README.md: register identity + artifact URIs only; per-task wiring is the split preparer’s job, not the dataset’s. - A new curation recipe →
recipes/README.md. One(publication, dataset)per recipe; shiprecipe.py(plusexplore.py— strongly recommended); confirm the upstream license againstrecipes/LICENSING.mdbefore writing it.