Skip to content

ci(proto): enforce Ledger protobuf conventions with a buf check plugin - #2300

Draft
flemzord wants to merge 2 commits into
poc/protocol-version-buf-checkfrom
poc/buf-ledger-lint-rules
Draft

flemzord wants to merge 2 commits into
poc/protocol-version-buf-checkfrom
poc/buf-ledger-lint-rules

Conversation

@flemzord

@flemzord flemzord commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Stack: #2298 (freeze at 40) → #2299 (frozen-revision check) → this PR. Review and merge in that order.

With the freeze, LEDGER_FIELD_NUMBERS_SEQUENTIAL and LEDGER_NO_RESERVED stay consistent: service-reachable types only gain fields (enforced by #2299), and persisted or Raft-only messages keep the remove-and-realign rule.

Summary

The "no reserved" and "sequential field numbers" rules from CLAUDE.md and
docs/technical/contributing/protobuf.md were enforced only by review. This
PR adds tools/buf-plugin-ledger, a buf check plugin
written in Go with buf.build/go/bufplugin, that enforces them through buf lint.

Rule Requirement
LEDGER_NO_RESERVED No reserved numbers or names in messages or enums
LEDGER_FIELD_NUMBERS_SEQUENTIAL Message field numbers are exactly 1..N, oneof members included; map entries exempt
LEDGER_ENUM_NUMBERS_SEQUENTIAL Enum values are exactly 0..N-1, no aliases

The current misc/proto tree has zero violations, so no baseline or ignore list is needed. Only the imported google/protobuf/descriptor.proto has reserved entries or gaps, and imports are not checked.

Wiring

  • just lint-proto → go run ./scripts/lintproto: builds the plugin into a temporary directory and runs buf lint misc/proto.
  • just pre-commit now includes test-buf-plugin and lint-proto. The required Dirty CI job runs just pre-commit, so these rules block merges without a ruleset change.
  • misc/proto/buf.yaml declares the plugin, and buf then needs it on PATH for breaking too. scripts/internal/bufplugin builds it for both lintproto and checkprotocolversion.
  • agent-check-pr: new proto-lint gate for misc/proto/** and tools/buf-plugin-ledger/**.
  • agent-check-pr fix: tools/*.go changes were selected as root-module test-race:./tools/... packages, which cannot work because each tools/* directory is its own Go module. They now go to the tooling gate; a selector regression case covers it.
  • The bufplugin dependency lives only in tools/buf-plugin-ledger/go.mod; the root go.mod is unchanged.

Example output

With a reserved 99; and a renumbered field injected into signature.proto:

misc/proto/signature.proto:17:1:message "signature.SignedApplyBatch" field numbers must be 1..3 without gaps; number 1 is missing. (buf-plugin-ledger)
misc/proto/signature.proto:18:12:message "signature.SignedApplyBatch" declares reserved entries; Ledger v3 deletes obsolete fields and realigns the remaining numbers. (buf-plugin-ledger)
lint-proto: FAIL

Validation

  • bash scripts/agent-check: PASS
  • go -C tools/buf-plugin-ledger test -race ./...: PASS (checktest fixtures for valid and invalid files, plus SpecTest)
  • go test -race ./scripts/agentcheckpr ./scripts/checkprotocolversion: PASS
  • golangci-lint on ./scripts/... and tools/buf-plugin-ledger: 0 issues
  • just lint-proto: PASS on the current tree; FAIL with the injected violations above
  • just tidy: no drift

https://claude.ai/code/session_01HxBUjBS5Loz1nkQZVxW1Mf

Add tools/buf-plugin-ledger, a buf check plugin with three lint rules
taken from docs/technical/contributing/protobuf.md and CLAUDE.md:

- LEDGER_NO_RESERVED: no reserved numbers or names;
- LEDGER_FIELD_NUMBERS_SEQUENTIAL: message fields are exactly 1..N;
- LEDGER_ENUM_NUMBERS_SEQUENTIAL: enum values are exactly 0..N-1.

The current tree has no violation. `just lint-proto` (scripts/lintproto)
builds the plugin and runs buf lint; it is part of `just pre-commit`, so
the required Dirty job runs it. Because buf.yaml now declares the plugin,
checkprotocolversion builds it too through scripts/internal/bufplugin.

agent-check-pr gains a proto-lint gate and now routes tools/* changes to
the tooling gate: those directories are separate modules that the root
module cannot race-test.

Claude-Session: https://claude.ai/code/session_01HxBUjBS5Loz1nkQZVxW1Mf
Comment thread justfile
test-buf-plugin:
go -C tools/buf-plugin-ledger test ./...

# --- Operator (Kubernetes) ---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [minor] Run the new plugin tests with the race detector

The new test-buf-plugin recipe runs go test ./... without -race; the proto-lint branch in scripts/agent-check-pr likewise omits it. Because the plugin is a separate module, the root-module tooling race suite does not cover it. Thus the newly introduced required gates can pass despite a plugin race, contrary to the Formance race-enabled test baseline.

Suggestion: Add -race to test-buf-plugin and to the plugin test invocation in scripts/agent-check-pr, preferably reusing the same recipe for both paths.

func firstMissing(numbers []int, start int) (int, bool) {
sorted := slices.Sorted(slices.Values(numbers))
for i, number := range sorted {
if number != start+i {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 [minor] Report an actually missing number for aliased enums

For a valid proto enum with numbers [0, 0, 1] and allow_alias enabled, firstMissing returns 1 at the duplicate, although 1 exists. checkEnumNumbersSequential consequently tells contributors that number 1 is missing; the actual missing number is 2. The same problem occurs with negative enum values. Rejection is correct, but the actionable diagnostic contradicts the descriptor.

Suggestion: Find the first absent number in the expected range using a membership set (or skip duplicates while scanning), and add tests for [0, 0, 1] and negative values.

@NumaryBot NumaryBot added risk: high NumaryBot classified this pull request as high risk. bot-reviewed NumaryBot completed its review workflow for the current head. draft-reviewed NumaryBot reviewed this draft, but published no readiness signal. labels Oct 15, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot-reviewed NumaryBot completed its review workflow for the current head. draft-reviewed NumaryBot reviewed this draft, but published no readiness signal. risk: high NumaryBot classified this pull request as high risk.

Development

Successfully merging this pull request may close these issues.

2 participants