Contributing¶
Everything runs inside the devcontainer.
It is a development environment and nothing else: the scheduled run and the upload happen in CI, from the runtime stage of the same Dockerfile, which is why the devcontainer stage is free to carry the test and lint tooling outright.
Setup¶
Open the repository in the devcontainer (Dev Containers: Reopen in Container) and you are done.
It is built from the repository’s own Dockerfile — the devcontainer stage of the image the scheduled run uses — so creating the container has only the development set, the local package in editable mode and the git hooks left to install.
Editing either that file or .devcontainer/devcontainer.json therefore means rebuilding the container before the change is real.
Node is in the image, so npx wrangler works for rehearsing an upload.
It is there because the deploy stage shells out to it, not because the pipeline needs a Node toolchain of its own.
Checks¶
pytest
ruff check .
black .
Tests use small fixtures in tests/fixtures/ and never touch the network — nor do they call a model.
Adding a test that needs either means the design has gone wrong somewhere; record the response as a fixture instead.
Formatting and linting are the git hooks’ job, and .github/workflows/quality.yml runs the same hooks on every push and pull request.
It is the same .pre-commit-config.yaml, so a commit that got through locally gets through there; the CI job skips taplo and shfmt, which only the devcontainer installs.
.github/workflows/tests.yml runs pytest alongside it, against requirements.txt rather than requirements-dev.txt.
That is the one check the devcontainer cannot reproduce, because the image installs the development set: a module in src/ that imports something only a development extra provides passes locally and fails there.
.github/workflows/image.yml builds the Dockerfile and runs a day inside it with the network switched off, and on main publishes it to GHCR.
The engine reaches every configuration repository as that image, and every failure mode of that lives in the build rather than in the code, which is why a pull request builds it too.
.github/workflows/docs.yml runs make docs, which treats every Sphinx warning as an error, so a broken link fails the pull request that made it; on main it publishes the site to GitHub Pages.
Conventions¶
The full conventions are in .github/copilot-instructions.md, which is the single source of truth whichever assistant is being used. Three of them shape most review comments:
Nothing personal in code, defaults, test fixtures or documentation. This repository is public; feed URLs and topics belong in a configuration repository.
Only the policy documents reach the model. A runner’s
editorial-policy.mdis loaded at runtime andsrc/curated_feed/prompt.mdships with the package;docs/scoring-spec.mdnever is.Only CI publishes.
curate-runstops atbuild/unless--deployis given, and the devcontainer holds no deploy credential. Tests may exercise the deploy stage but must never run a command: inject the runner, astests/test_deploy.pydoes.
The version comes from annotated git tags via setuptools_scm, following semantic versioning, and commit messages follow Conventional Commits, enforced by pre-commit.
Releasing to configuration repositories¶
Every merge to main publishes an image, tagged with its version, sha-<commit> and latest.
None of that reaches a configuration repository by itself, which is the point (docs/ARCHITECTURE.md, D10):
Copy the digest from the summary of that
imagerun into thecontainer:line ofcurate.yml, in a pull request of its own.Each configuration repository moves its
uses: …/curate.yml@<commit>to the commit that merged it, whenever it chooses to.
What curate.yml accepts — its inputs, its secret names, the feeds/<name>/ layout and the cache paths — is what every caller depends on.
Changing any of it is a breaking release, and the cache paths especially: a caller whose paths move loses its corpus and published log without an error.
Where things are written down¶
Document |
Holds |
Read it when |
|---|---|---|
|
what it is, and a map of the rest |
deciding whether to use it |
|
how to get it running, and how to run it day to day |
setting it up, or operating it |
|
every command and its options, rendered from the parsers |
using a command; to change it, edit its |
|
what each part of the system can see |
changing what leaves the machine |
|
why it is shaped this way, the contracts, and the invariants |
changing how it works |
|
the ranking mechanics as implemented |
changing |
|
how the model is told to judge an article |
changing what the model returns |
|
every setting a runner can have, annotated |
adding or changing a setting |
|
the shape of a policy, and what the prompt relies on in it |
changing how a policy is read |
|
what one runner’s day does, and what callers depend on |
changing how it is published |
Managing requirements¶
Direct dependencies live in pyproject.toml.
Two pinned files are generated from it and should not be edited by hand:
requirements.txt— what the application itself needs. This is what the CI job installs.requirements-dev.txt— the above plus the development extras, and what creating the devcontainer installs.
After changing a dependency, recompile both and reinstall:
make reqs && make deps
To upgrade everything, add --upgrade to the uv pip compile commands in the reqs target, then make deps.
Dependabot proposes upgrades weekly (.github/dependabot.yml), editing the pinned files directly.
That is safe — recompiling prefers what is already written there — but it cannot widen a lower bound in pyproject.toml.
An upgrade that needs one is a commit of its own.
Make commands¶
target |
effect |
|---|---|
|
compile |
|
install pinned requirements and the local package |
|
build the documentation into |
|
create and push the next version tag |
|
delete caches and compiled files |
|
list all targets |