Skip to content

Contributing

This page is for working on Open Meridian's public repositories: meridian-core, meridian-cli, meridian-python, meridian-schema, meridian-snaptrade and meridian-docs. It covers how each is checked, the rules every change keeps, and how to raise a bug or an idea.

Issues, not pull requests

Open Meridian is open source, and built by its team: we don't merge pull requests from outside it. To help, open an issue on the repository -- Report a bug or Suggest an improvement -- and describe the problem or the idea rather than pasting code; we build every change ourselves. Opening an issue gives Societal Lab Inc. a perpetual, irrevocable, royalty-free licence to use what you suggest, as each repository's CONTRIBUTING.md says. Plugins, which live in your own repositories, are the way to build on Open Meridian.

The gate: make ci-local

Every public repository has one command that runs every check it has:

make ci-local

A green local run is the completion signal. CI is confirmation, not the place a failure is first discovered: every job CI runs is reachable from ci-local, so nothing fails there that could not have failed on your machine.

On a fresh clone, run this once:

make install-hooks

It points git at the repository's hooks, so git push runs make ci-local first. Without it the gate exists and does not run. In meridian-core, a change touching a Dockerfile, a lock file, a CI workflow or a .proto is promoted to the longer ci-local-deep automatically.

The toolchains are pinned inside containers, so Docker is the only thing the host needs. A host protoc or Rust toolchain at a different version would produce subtly different output, so the host is never asked to have one.

Repository Also useful
meridian-core docker compose up runs every component locally, with a database and a broker
meridian-schema make codegen regenerates the Rust and Python bindings, in a pinned container
meridian-python Conformance tests assert against the same pinned message bytes as the Rust runtime, so the two agree with the contract rather than with each other

Rules every change keeps

These are enforced in review, and several by the gate.

Exact decimal for money. Never floating point for a quantity, a price or a balance, at any layer — including an adapter reading a third-party API. Quantities and money cross the wire as integers scaled by 10⁸; in the Python SDK they are Decimal, and a value that cannot be carried exactly is refused rather than rounded. Convert once, at the boundary, on the way in. This is the class of code where a rounding error becomes a reconciliation break.

Plugins hold no state. No local database, no file a plugin expects to find again. A plugin can be killed and replaced at any moment and seeds from the deployment when it starts.

A plugin talks only to its sidecar. Access control, encoding, health and lifecycle live in the sidecar, once. The SDK is a thin client for it: anything added to the SDK must be a convenience, never a decision, because a decision made in the SDK is made once per plugin and wrong in a different way each time.

Typed operations are generated, never hand-written. The typed methods a plugin's roles may call are derived from Open Meridian's contract. Generate them, or do not have them yet.

Generated files are never edited by hand. They are committed and reviewed like any other code, and make check-codegen regenerates them into a scratch directory and fails on any difference.

No third-party data models in the kernel. The deployment's own core defines its own types. External standards are translated at the plugin boundary.

A plugin never creates reference data. When an instrument does not resolve, a plugin reports the miss and moves on; it does not block, retry in a loop, or mint an instrument. See Instruments.

Resolution is dated. Ask what an identifier meant on a date. An undated lookup is a bug waiting for the day an identifier is reassigned.

Credentials come from the environment. Never a literal, never a fixture, never a committed configuration file.

The contract changes only when a workflow demands it

Every message in meridian-schema, and every domain message in meridian-core's proto/, exists because a written workflow step needs it. Nothing enters the contract ahead of that, and the gates fail a message that nothing justifies.

So a change to a .proto, or anything that would change what a plugin may send or receive, is not an ordinary pull request. Open an issue describing what a plugin or a person needs to do and cannot; the contract follows from that, not the other way round.

Licences, and why a plugin stays yours

meridian-core and meridian-cli are AGPL-3.0-or-later. meridian-python and meridian-schema are Apache-2.0. A plugin links only the SDK and the contract — it talks to nothing but its sidecar — so a plugin you write on them stays yours, whatever licence you choose for it. Keep that line where it is: a change that would make a plugin link anything AGPL is a change to that promise. See Repositories.

Review

Every pull request gets the same review passes in the same order, whether a person or an AI agent wrote it: correctness first, then security, then whether it respects the contract, then whether it matches what it set out to do. Formatting, link checks and code generation are left to make ci-local, not to reviewers; a thing a reviewer catches that a gate could have caught is a missing gate.