Contributing to kuration-sdk¶
Thanks for helping improve kuration-sdk. Contributions may include bug fixes, adapters, selection methods, documentation, tests, and runnable examples.
For user and integrator questions, see SUPPORT.md. Project ownership, decisions, and release responsibilities are described in GOVERNANCE.md.
Development setup¶
kuration-sdk requires Python 3.11 or newer. The supported-version CI matrix explicitly covers Python 3.11 through 3.14. From a repository checkout, create and activate a virtual environment, then install the package in editable mode:
python -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
Install every optional runtime integration when working across adapters, containers, learned methods, or framework support:
Optional dependencies must remain optional. The base install depends only on NumPy; imports of heavy integrations should be lazy and should raise a clear installation hint when unavailable.
Design rules¶
- Keep the execution framework domain-neutral. First-party domain behavior may ship as a namespaced built-in adapter, analyzer, operator, or transform when it has an owner, a closed contract, focused tests, documentation, and lazy optional dependencies. External or independently versioned behavior belongs behind the same extension seams as a plugin.
- Validate configuration early and loudly. Unknown fields, operators, methods, and parameters must not silently fall back.
- Preserve deterministic results across worker counts for a fixed seed.
- Keep written datasets reproducible: changes affecting execution must be represented in
config.jsonand, when applicable, versioned or migrated. - Preserve pipeline immutability: every operator returns a new
Pipeline. - Prefer
.bin()and.sample()in new pipeline code..organize()and.curate()are compatibility aliases and must not be removed without a documented migration.
See docs/architecture.md for the data contracts and docs/customization.md for supported extension points.
Making a change¶
Add focused tests with every behavior change. A regression test should fail before the fix and exercise the public interface where practical. Tests that should produce data must use loud error handling and assert that their candidate and output pools are non-empty.
Keep public interfaces synchronized across:
- generic implementation and exports in
src/kuration/core/; - maintained domain implementation in
src/kuration/gameplay/; - exact signatures in docs/api.md;
- pipeline behavior in docs/pipeline.md;
- runnable examples when the recommended workflow changes;
- artifact or job schema versions when serialized contracts change.
New formats and optional integrations should include tests that skip cleanly when their extra is not installed. A missing optional dependency at runtime should identify the extra needed to use the feature.
Choosing an extension seam¶
Use the narrowest supported seam for the behavior:
| need | extension |
|---|---|
| Reuse a named sequence of pipeline calls | compositional operator |
| Read a new source format | input adapter |
| Compute bounded model/device features over batches | batch transform |
| Compute one scalar from one segment window | registered metric |
| Score segments using a whole pool, model, or reference set | segment scorer |
| Accumulate state across a stream or audit a written artifact | analyzer |
| Derive an output tensor or encoded value | output encoder |
| Change how segments receive selection priority | selection method |
| Remove segments before selection | pre-pass filter |
| Store and read shards in another format | container |
Package a reusable OutputSpec policy |
output preset |
Do not add dataset-specific parsing, model imports, or storage branches to the generic pipeline when one of these seams fits. See docs/customization.md for descriptors, isolated registries, discovery, and versioning.
Compositional operators¶
An operator names a validated sequence of existing pipeline calls. It does not create a new execution stage:
from kuration import register_operator
@register_operator(
"acme/balance-signal",
description="Measure and balance signal intensity",
parameters={
"type": "object",
"properties": {
"field": {"type": "string"},
"bins": {"type": "integer", "minimum": 1},
},
"required": ["field"],
"additionalProperties": False,
},
version="1",
)
def balance_signal(pipeline, *, field, bins=8):
return (
pipeline
.measure("signal_level", "mean", of=field)
.bin("signal_level", bins=bins)
)
pipeline = pipeline.balance_signal(field="signal", bins=6)
The handler must return a pipeline derived from its argument and add at least one ordinary
operator. Add tests for parameter validation, immutable expansion, resolved strategy, and the
operator/expansion provenance stored in config.json.
Input adapters¶
An adapter converts one source into ordered canonical Tick values:
from kuration import Tick, register_adapter
class AcmeLogAdapter:
supports_streaming = True
def __init__(self, schema, *, reader):
self.schema = schema
self.reader = reader
def check_source(self, source_uri):
self.reader.probe(source_uri) # metadata-only validation when possible
def iter_ticks(self, source_uri):
for row in self.reader.rows(source_uri):
yield Tick(
time=row.timestamp,
entities={row.agent_id: {"signal": float(row.signal)}},
)
def ticks(self, source_uri):
return list(self.iter_ticks(source_uri))
register_adapter(
"acme/log",
AcmeLogAdapter,
description="Read ACME telemetry logs",
capabilities={"streaming": True},
version="1",
)
Validate mapped fields, lengths, shapes, ordering, and source metadata loudly. Keep imports for
optional parsers inside the adapter and provide an extra plus install_hint descriptor. Test
both ticks and iter_ticks parity when streaming is supported, metadata-only check_source,
multiple entities, malformed input, and workers>1 pickling/plugin reconstruction.
Metrics and scorers¶
Use a metric for stateless, per-window scalar computation:
import numpy as np
from kuration import register_metric
@register_metric("acme/peak-signal", description="Maximum signal in a segment", version="1")
def peak_signal(window):
return float(np.nanmax(window.numeric_field("signal")))
Use a scorer when computation needs the full segment pool, model state, or raw window buffers:
import numpy as np
from kuration import register_scorer
class QualityScorer:
needs_buffers = True
def __init__(self, *, scale=1.0):
self.scale = float(scale)
def score(self, segments, ctx):
values = [np.nanmean(buf.numeric_field("signal")) for buf in ctx["buffers"]]
return self.scale * np.asarray(values, dtype=np.float64)
register_scorer("acme/quality", QualityScorer, version="1")
Metrics and scorers must return finite scalars. Registered metrics used with multiprocessing
must live in an importable module, not only in __main__. A scorer returns exactly one value per
segment; declare needs_buffers = False when it uses only existing segment features so the SDK
does not reread source windows.
Batch transforms and media¶
Use a registered batch transform for bounded model/device work over ordinary fields, existing features, or lazy media. Declare scalar outputs and semantic media requirements at registration:
from kuration import register_batch_transform
@register_batch_transform(
"acme/frame-quality",
outputs=("frame_quality",),
inputs=("media:frame",),
media_requirements={"kind": "video-frame", "layout": "HWC", "color_space": "RGB"},
resources={"num_gpus": 1, "preferred_batch_size": 16},
)
class FrameQuality:
def __init__(self, *, device="cpu"):
self.model = load_model(device) # optional framework import/load belongs here
def transform_batch(self, batch, ctx):
return {"frame_quality": self.model(batch["media:frame"])}
Outputs must be one finite scalar per input segment. Test bounded batch sizes, deterministic segment alignment, media semantic mismatches, missing optional dependencies, and CPU parity when the implementation supports CPU. Never silently change model, device, dtype, or precision.
Output encoders¶
An encoder derives declared encoder: modalities from one output record, or implements
encode_batch(records, ctx) for bounded batched encoding:
import numpy as np
from kuration import register_encoder
class ActionEncoder:
def timestep(self, record, ctx):
return {"action": np.asarray(record["action"], dtype=np.float32)}
register_encoder("acme/action", ActionEncoder, version="1")
The corresponding modality uses "source": "encoder:action". Test exact dtype and shape,
missing values, padding, round trips through the intended container, and reconstruction by name
for declarative jobs. Encoders must not mutate input records.
Methods and filters¶
A custom method returns one finite priority per segment, with higher values selected first:
import numpy as np
from kuration import register_method
def newest_first(segments, strategy, *, seed=0, workers=1):
return np.asarray([segment.start_idx for segment in segments], dtype=np.float64)
register_method("acme/newest", newest_first, capabilities={"whole_pool": True}, version="1")
A filter returns retained Segment objects in deterministic order:
import math
from kuration import register_filter
def finite_features(segments, feature_names, *, seed=0, workers=1):
return [
segment for segment in segments
if all(math.isfinite(segment.features[name]) for name in feature_names)
]
register_filter("acme/finite", finite_features, version="1")
Declare a parameter JSON Schema for any method/filter knobs. Test empty and tiny pools, determinism across worker counts, output shape/order, non-finite failures, and interaction with predicates, tag coverage, group caps, and hours-based filling when relevant.
Containers and presets¶
Register a container with a filename extension and inverse writer/reader callables:
import numpy as np
from kuration import register_container
def write_acme(path, arrays, n_steps):
with open(path, "wb") as stream:
np.savez(stream, _n_steps=np.asarray(n_steps), **arrays)
def read_acme(path):
with np.load(path) as stored:
arrays = {name: stored[name] for name in stored.files if name != "_n_steps"}
return arrays, int(stored["_n_steps"])
register_container(
"acme/tensor",
extension=".acme",
writer=write_acme,
reader=read_acme,
version="1",
)
Container writers receive the final path, modality arrays, and real unpadded length. Readers
must return (arrays, n_steps). Test scalar and higher-rank tensors, short/padded shards, remote
path behavior if advertised, resume, manifest loading, and dtype/shape round trips.
An output preset is a factory (schema, **parameters) -> OutputSpec registered with
register_preset. It should assemble ordinary declared modalities rather than bypass output
validation. Include a closed parameter schema and test strict categorical behavior.
Packaging external plugins¶
Package extensions that should remain independently versioned or distributed outside
kuration-sdk as their own installable Python package. Registration code may be loaded explicitly
through a job's plugins list or automatically by the kuration.plugins entry-point group:
The target may be a module that registers on import or a zero-argument registration function.
Keep plugin imports lightweight and lazy-load optional frameworks inside factories or execution
methods. Supply descriptor metadata such as description, parameters, capabilities, extra,
install_hint, package, version, documentation, and stability. Verify discovery with:
Plugin tests should start in a clean process so registration/import behavior matches spawned
workers and installed entry points. They must also cover an isolated Registry; use
"include_plugin_entry_points": false in declarative-job tests that must not depend on whatever
other plugins happen to be installed.
Checks¶
Run the same core checks as CI:
python -m pytest tests/ -q
python -m compileall -q src/kuration examples
python -m ruff check src tests examples
python -m mypy
python -m build
CI additionally installs the built wheel into a clean environment and verifies that kuration,
every installed kur-* console command, schemas/job.schema.json, and py.typed work without
the repository on sys.path.
For changes spanning the pipeline or output path, also run the relevant self-contained examples:
python examples/quickstart.py --out quickstart-out
python examples/01_timeseries_to_shards.py
python examples/02_robotics_action_aware.py
python examples/03_compare_strategies.py
python examples/04_operator_pipeline.py
python examples/05_custom_operators.py
The examples synthesize their own data. Do not commit their generated shards or manifests.
Documentation and style¶
Use Python 3.11-compatible syntax and keep lines within 100 characters. Follow the existing module style, use type hints where they clarify public contracts, and write actionable validation errors. Documentation has one canonical home; summarize and link instead of duplicating a full explanation across pages.
Any new public object exported through kuration.__all__, or any new public Pipeline method,
must be documented in docs/api.md. User-facing behavior should include a minimal example or a
link to a runnable one.
Pull requests¶
Keep pull requests focused and describe:
- the problem and intended behavior;
- the compatibility or artifact-format impact;
- the extension kind affected (adapter, analyzer, container, encoder, filter, method, metric, operator, preset, scorer, or transform);
- tests and examples run;
- optional dependencies or compute implications;
- documentation updated.
Before requesting review, confirm that unrelated generated files are absent and that all relevant CI checks pass. Breaking changes before 1.0 must still be deliberate, documented, and accompanied by migration guidance when they affect public APIs, jobs, or stored artifacts.
Releases¶
Contributors should not publish package artifacts. Maintainers release through the trusted PyPI
workflow described in docs/releasing.md. The project license and author
metadata (Apache-2.0, see LICENSE) are recorded in pyproject.toml.