Contributing
This repo has a strong house style — it is what keeps a codebase with this many guarantees reviewable. Read this page before your first PR; most of it is enforceable by CI, but the commit message convention and the comment voice are reviewed by humans.
Dev workflow
- Branch from
main; keep one logical change per commit — the git log reads as a series of self-contained rationales, and bisectability depends on it. - Run the fast loop before pushing:
go build ./...,go test -race ./...(CI runs the race build on every push), andbash scripts/bench-gate.shif you touched a hot path. - User-visible changes get a
CHANGELOG.mdUnreleased entry at commit time, not at release time. Entries carry aChanged/Added/Fixed/Removedheading and the same paragraph voice as the commit — the Settled→Commit rename entry, for example, includes the complete old→new mapping because external consumers needed it to migrate.
The commit message convention
This repo’s commits are single, detailed, paragraph-led messages written in
the imperative that explain WHAT and WHY, with concrete file and identifier
references. No one-line subjects, no bullet laundry lists (a short list is
acceptable inside the paragraph when enumerating parallel changes). Read a
few from git log — e.g. the Rename Settled → Commit everywhere commit or
the Promote transforms to registered plugins commit — they record the
motivation, the ruling that decided contested points, the migration/compat
story, and the verification done (which tests were run). A good test: could
a reviewer reconstruct the change from the message alone in a year?
Rules of thumb distilled from the log:
- First sentence = the change, imperative (“Add …”, “Fix …”, “Remove …”, “Rename …”).
- Then the why, naming the ruling or review finding that decided it (e.g. “review R6”, “M3-audit J2 ruling”) — see Design discussion below.
- Then the mechanics, with real file paths and identifiers
(
internal/engine/commit.go,RegisterTransformT,plugin_schema). - End with what was verified (“full suite passes incl. -race on the engine”).
Code conventions
gofmt and golangci-lint clean.
.golangci.ymlis deliberately small: the standard set (errcheck, govet, ineffassign, staticcheck, unused) plus misspell and gofmt — a hygiene floor, not a style machine. Exclusions (generated stubs inpkg/pluginproto, the vendored CESQL TCK, a quickfix class) must be justified in the config; new linters join with a ruling.Comment voice: comments state constraints and rationale, never narrate the obvious. A representative example,
internal/registry/builtin/kafka.go:Commit runs on every frontier advance (~per message), so the scan must start at the watermark, not at 1: the scan itself deletes every seq it visits, which keeps each call O(new work) instead of O(all emissions).
The comment gives the constraint (called per message), the decision (scan from the watermark), and the consequence (O(new work)). “increments the counter” comments do not exist here. If a comment only restates the code, delete it; if a constraint lives only in a review document, move the essential sentence into the code.
The full-name principle for YAML vocabulary. Config keys are full words in snake_case —
max_in_flight,drain_timeout,skip_if_successful,catchup_window,span_sample_rate— never abbreviations. The single recorded exception isdlq(retained industry-wide, spec v1.6). New keys follow the same rule; if you are tempted to abbreviate, the key is probably too long.Error wrapping with
%wso classification survives: the engine unwrapsregistry.TransformErrorwitherrors.As, and store/registry errors are matched by identity, not string comparison. Wrap, never re-format, an error you pass along.
The “when you touch X you must also Y” checklist
| You touch… | You must also… |
|---|---|
| a plugin config struct (new field) | add schema tags; regenerate the goldens (go test ./internal/registry/builtin -update-schemas); check whether a runtimecfg/registry test pins the unknown-key behavior |
| a CLI flag or usage string | regenerate help goldens (go test ./internal/cli -update) |
| a metric | add the instrument in internal/obs/obs.go; record it where the event happens; this developer guide’s metrics table |
| a diagnostic code | add it to the diagnostics table in Configuration & diagnostics; codes are API — do not rename, retire with a mapping in the CHANGELOG |
| the engine’s delivery/commit paths | keep the eight invariants green and run -race; extend the relevant TestInvariant_* scenario if you changed a guarantee |
| the spool/checkpoint format or retention | the retention tests (internal/engine/retention_test.go) and the kill-9 replay invariant |
| a user-visible behavior | a CHANGELOG Unreleased entry in the same commit |
Adding a builtin plugin
The checklist for a new compiled-in plugin (source, sink, codec or transform) — every step has a test that fails until you do it:
- Define one config struct:
jsontags name the keys,schematags the constraints (Plugin system). - Write the register function (typed:
RegisterSourceT/RegisterSinkT/RegisterCodecT/RegisterTransformT) and wire it intoRegisterAll(internal/registry/builtin/register.go). Cross-field rules JSON Schema cannot express stay in the build function. - Regenerate the schema goldens (
go test ./internal/registry/builtin -update-schemas) and review the diff. - Catalog surfaces are automatic —
plugin catalog,plugin schema, the MCPcatalogtool and the LSP completion read the registry — but their tests may need the new entry. - Codecs add a conformance case (
codecs_conformance_test.go); the protobuf codec’s descriptor fixture regenerates with-update-descr. - Remember the name rules: the plugin name must not collide with framework fields (registration rejects it) and should follow the full-name principle below.
PR expectations
- Full suite green (
go test ./...), with-raceon the engine and store packages (CI does this; run it locally first). - The examples gate stays green: every pipeline under
examples/must verify and pass its contract suites (internal/cli/examples_test.goruns it in CI). - Golden diffs (help screens, plugin schemas) present and reviewed line by line — a schema golden diff that “just regenerates” without discussion is a red flag.
- New public surface (CLI verb, metric, config key, diagnostic code) follows the checklist above.
Where design discussion lives
redesign-v3.md is the spec and the single source of truth. Design
discussion has a discipline: contested points are resolved as numbered
rulings recorded in the spec’s revision log (and echoed in README
decision ledgers), citing the review round that raised them
(redesign-v3-review*.md). If your change settles a design question, add
the ruling — reviewers should be able to trace every non-obvious decision
to a dated entry. Dated review documents keep their historical wording even
after later renames (the Settled→Commit rename explicitly left them alone).
Licensing
Eventboat is Apache-2.0 (LICENSE). By contributing you agree your
contributions are licensed under the same terms. Keep the generated protocol
stubs in pkg/pluginproto regenerable from
proto/eventboat/plugin/v1/plugin.proto — do not hand-edit them.