Skip to content

feat: a payload schema for three kinds, with presence declared field by field (ADR 0074) - #112

Merged
kfeofantov merged 1 commit into
mainfrom
feat/protobuf-schema-three-kinds
Sep 7, 2026
Merged

kfeofantov merged 1 commit into
mainfrom
feat/protobuf-schema-three-kinds

Conversation

@kfeofantov

Copy link
Copy Markdown
Contributor

ADR 0002 promised public .proto files and none existed, so the golden payload
corpus became the contract by default. This writes the schema for the three
kinds that have been read closely — node_metadata, collection_coverage,
node_lifecycle — and deliberately leaves the other nineteen alone: a .proto
written from a struct freezes a guess for an N-2 window.

Nothing goes on the wire. No payload byte moves, no encoder changes, no
golden file is touched.

The decision that shapes the files

Proto3 singular scalars have no presence: a measured zero and an unreported
field are the same bytes. "malformed": 0 is a claim, and as a plain int64 it
becomes indistinguishable from an agent that said nothing.

So presence is declared field by field, asking of each whether silence and
nought mean the same thing. optional on every capacity/allocatable number,
every coverage counter, every config switch, SourceHealth.synced/.failing,
and at_observed — where false and absent coincide today only by coincidence.
Plain where the empty value is the absence: kind, source, label strings,
window_seconds, and the two knobs whose zero means "the default applies".

No enums: canonical JSON renders an enum by its declared name, which is not
"joined" or "NoSchedule". Integer widths are 64-bit throughout, so
capacity_cpu_milli and capacity_memory_bytes render alike rather than one as
a number and one as a string. The goldens do not move to canonical proto JSON.

Full reasoning, including the JSON representation and the compatibility rules,
in ADR 0074.

What keeps it honest

internal/pb/kubernetesv1 parses each covered golden with DiscardUnknown off,
then walks the golden JSON beside the parsed message and fails on any key whose
value did not survive as a set field. The check is not vacuous:

$ # optional removed from intake_rejections.malformed
$ go test ./internal/pb/...
corpus_test.go:62: collection-coverage.golden.json.intake_rejections.malformed:
  the corpus states 0 and the parsed message reports the field unset;
  a zero the corpus states is a claim, so declare `optional` on it (ADR 0074)

A third test logs the nineteen uncovered kinds, so the gap is a number in test
output rather than an assumption.

buf lint and buf breaking against main run in CI. Verified against a
baseline branch:

$ buf breaking --against '.git#ref=proto-baseline'      # unchanged
$ # field 19 retyped
node_metadata.proto:95:12: Field "19" with name "capacity_pods" on message
  "NodeInfo" changed type from "int64" to "string".
$ # field 3 deleted without reserving it
collection_coverage.proto:211:1: Previously present field "3" with name
  "taints_dropped" on message "NodeDrops" was deleted.

The breaking step is guarded on main carrying a schema, because on this PR it
does not yet.

Verification

From a clean git archive HEAD extraction:

$ go test ./...                 # 23 packages ok, exit 0
$ buf lint && buf format -d     # clean
$ golangci-lint run             # 0 issues
$ make vulncheck                # No vulnerabilities found

Notes for review

  • backend-requirements.md is untouched, and not by preference.
    docs/prose_test.go caps it at 733 lines and it sits at exactly 733, so one
    added line fails the build. The obligation an ingest implementer needs — read
    an absent counter as unreported, never as zero — lands with the transport
    that carries it. Stating it now is a ceiling raise, which is deliberately a
    decision in its own diff.
  • buf is not a Go tool. It would pull in a large tree; CI installs it via
    bufbuild/buf-setup-action and make proto expects it on PATH.
    protoc-gen-go is wired as a Go tool — the module was already a dependency,
    so go.mod gains one line and go.sum nothing. Generated Go is committed, so
    go build and go test need neither.
  • docs/security.md is untouched: nothing about what is collected, stored or
    transmitted moved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA

…by field (ADR 0074)

ADR 0002 promised public `.proto` files and none existed: the corpus of golden
payloads became the contract by default. This writes the schema for the three
kinds that have been read closely — `node_metadata`, `collection_coverage`,
`node_lifecycle` — and leaves the other nineteen alone.

Proto3 singular scalars have no presence, so a measured zero and an unreported
field are the same bytes. Every field whose zero or false is a claim is declared
`optional`; every field whose empty value *is* its absence is not.

Nothing goes on the wire: no payload byte moves and no encoder changes. The
corpus is what keeps the schema honest — each covered golden is parsed into its
generated message with `DiscardUnknown` off, and a second walk fails on any
value that did not survive as a set field.

    go test ./... && golangci-lint run   # all green
    buf lint                             # clean

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KeZECJXTDHL6Nts95eKMJA
@kfeofantov
kfeofantov merged commit 42257a2 into main Sep 7, 2026
4 checks passed
@kfeofantov
kfeofantov deleted the feat/protobuf-schema-three-kinds branch September 7, 2026 18:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant