Contributing and documentation policy¶
Development¶
uv sync --extra all --extra gpu --group dev --group docs # or --extra cpu without an NVIDIA GPU
uv run pytest -q
uv run ruff check src tests examples
uv run mkdocs serve # live docs at http://127.0.0.1:8000
CI runs ruff, pytest and mkdocs build --strict on every push and pull request. Docs
deploy to GitHub Pages from main.
Documentation must move with the code¶
The docs are part of the definition of done. Any change that adds, removes or renames a user-visible thing updates the relevant page in the same commit:
| change | update |
|---|---|
| new / removed / renamed op | docs/reference/ops.md |
| new / removed frontend or URL | docs/concepts/formats.md |
| REST endpoint | docs/reference/api.md |
| CLI command or flag | docs/reference/cli.md |
| caching behaviour | docs/concepts/caching.md |
| control page / viewer / events | docs/concepts/interactivity.md |
| architectural decision | docs/design.md |
| roadmap item shipped | remove from docs/roadmap.md, document where it landed |
Two mechanisms enforce this:
tests/test_docs.pyfails if a registered op is missing from the ops reference, a frontend is missing from the formats page, or a REST route is missing from the API reference.mkdocs build --strictfails on broken links and missing pages.
CLAUDE.md at the repo root restates this policy for AI-assisted sessions.
Adding an op¶
from chunkmirage.ops import Op, register
from pydantic import Field
import numpy as np
@register
class MyOp(Op):
"""One-line description shown by `chunkmirage ops`."""
name = "my_op"
halo = 4 # voxels of upstream context needed per side (int or per-axis tuple)
cache = False # True for expensive stages (inference)
strength: float = Field(1.0, description="What it does, in words a user understands") # shown in the UI
def output_dtype(self, in_dtype): return np.dtype("float32")
def apply(self, block): return block.astype(np.float32) * self.strength
Register it in pyproject.toml under [project.entry-points."chunkmirage.ops"] (or in your
own package's entry points), add a test, and add a row to docs/reference/ops.md.
Adding a source¶
import numpy as np
from chunkmirage.core import ArrayInfo, Box
from chunkmirage.sources import MultiscaleSource, Source
class Ramp(Source):
def __init__(self, n):
self._info = ArrayInfo((n, n, n), np.float32, (64, 64, 64), (8, 8, 8), ("nm",) * 3, ("z", "y", "x"))
@property
def info(self): return self._info
def read(self, box: Box): # exactly box.shape, within info.shape
z, y, x = np.mgrid[box.slices()]
return (x + y + z).astype(np.float32)
def open_ramp(url: str) -> MultiscaleSource: # ramp://512
return MultiscaleSource([Ramp(int(url.split("://")[1]))], name="ramp")
List it under [project.entry-points."chunkmirage.sources"] as ramp = "mypackage:open_ramp"
(see formats). Sources
built into chunkmirage are listed in chunkmirage.sources.registry instead, and documented
in docs/concepts/formats.md.
Plugin API and versioning¶
Packages build on chunkmirage through its plugin API, which is stable:
- Ops.
Opand its contract: the class attributesname,halo,cache,packages,output_kindandslots; the methodsapply,apply_at,output_dtype,output_info,input_voxel_size,for_levelandcache_token; parameters as pydantic fields.registerand thechunkmirage.opsentry point. - Sources.
Source(info,read,read_padded,cache_key),ChunkedSource,MultiscaleSource,ArrayInfoandBox; an openeropener(url, *, cache_bytes, cache)returning aMultiscaleSource, registered withregister_sourceor thechunkmirage.sourcesentry point;open_source. - Serving.
PipelineSpec,Pipeline,create_appand its parameters,serveandServer,DatasetRegistry(add,get,resolve,refresh,remove,subscribe) and its resolver, thechunkmirage.routesentry point, the REST routes in the API reference, the served URL layout, and the CLI's flags.
Everything else may change in any release: names starting with _, modules not named
above (chunkmirage.fused, the frontends' internals), log messages, the control page and
the browser engine.
Versions follow semantic versioning. Before 1.0, a minor release (0.x.0) may change the stable API, but only with a changelog entry under its own heading saying what to change in a plugin, and, where it can, after one minor release in which the old way still works and warns. A patch release (0.x.y) never breaks it. From 1.0, a change to the stable API waits for a major release.
Releasing¶
- Move the changelog's unreleased entries under the new version, and set that version in
pyproject.tomlandchunkmirage/__init__.py(tests/test_docs.pychecks the three agree). - Merge to
mainwith CI green. - Publish to PyPI only when the owners say so:
uv build && uv publish.