Skip to content

Add a compatibility page and pin the exported API surface - #244

Merged
skyoo2003 merged 5 commits into
mainfrom
compatibility-promise
Aug 10, 2026
Merged

skyoo2003 merged 5 commits into
mainfrom
compatibility-promise

Conversation

@skyoo2003

Copy link
Copy Markdown
Owner

Summary

kvs exposes three wire protocols, a CLI, and a Go package, and nothing said which of them a
future release would keep. This adds the page that says so, and a test that keeps the Go half
of the answer honest.

No behaviour changes.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Refactor
  • CI/CD

Changes

  • content/docs/compatibility.md — what a v1 tag will promise not to break (HTTP, gRPC,
    RESP2, the CLI flags, and the root Go package), what it deliberately leaves out, and how
    something gets removed if it has to be. It also writes down the trust boundary as it stands:
    no authentication, no authorization, no TLS, HTTP and gRPC on every interface by default
    while RESP stays on loopback.
  • api_surface_test.go and testdata/api-surface.txt — TestPublicAPISurface renders every
    exported declaration in the root package and compares it against the golden file, so a
    symbol cannot join or leave without the diff landing in review. Comments are left unparsed
    and function bodies dropped, which keeps the file about signatures and nothing else.
    Regenerate deliberately with go test -run TestPublicAPISurface . -update.
  • replication.go and pkg/resp/reader.go — SetReplicator, ReplaceWith, and
    ApplyReplicated are exported so internal/cluster can reach them across the package
    boundary, and pkg/resp exists so the server can speak RESP2 rather than as a library for
    other programs. Both now say so in godoc.
  • README.md, content/docs/overview.md — link to the new page. contributing.md and
    release.md shift one place in the sidebar to make room for it.

Testing

  • make all passes
  • New tests added (if applicable)

Checked that the guard actually guards: adding a dummy exported function fails
TestPublicAPISurface with the offending line, and removing it passes again.

Related Issues

None.

@github-actions github-actions Bot added documentation Improvements or additions to documentation go Pull requests that update Go code labels Aug 9, 2026
@skyoo2003 skyoo2003 self-assigned this Aug 9, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d7fb304c93

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread content/docs/compatibility.md Outdated
Comment on lines +18 to +19
All three protocols share one keyspace, and that stays true: a value written over HTTP is
readable over RESP and gRPC, and the reverse.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Qualify interoperability to string values

Limit this promise to string values: RESP supports lists, hashes, sets, and sorted sets, but the HTTP and gRPC GET handlers return an internal error when the stored value is not a string. For example, after LPUSH key value, neither HTTP nor gRPC can read key, so the stated reverse interoperability is already false.

Useful? React with 👍 / 👎.

Comment thread content/docs/compatibility.md Outdated

## Trust boundary

**kvs has no authentication, no authorization, and no TLS.** Anyone who can open a connection

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Document the existing RESP authentication

Describe the listeners separately rather than saying kvs has no authentication: setting resp_password or KVS_RESP_PASSWORD makes the RESP server require AUTH, and cluster join requests authenticate through that listener as well. The statement is therefore false whenever an operator configures the already-documented RESP password; only HTTP and gRPC are always unauthenticated.

Useful? React with 👍 / 👎.

Comment thread testdata/api-surface.txt

func (e *NotLeaderError) Is(target error) bool

func (s *Store) SetReplicator(replicate func(fn func(tx *Tx) error) error)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exclude cluster plumbing from the promised surface

Remove the exempt plumbing methods from this promised golden surface, or track them separately. The compatibility page explicitly allows SetReplicator, ReplaceWith, and ApplyReplicated to change or disappear in a v1 minor release, but their presence here makes TestPublicAPISurface fail such a permitted change with the message that it requires a major version.

Useful? React with 👍 / 👎.

Comment thread api_surface_test.go

fmt.Fprintf(&buf, "==== %s ====\n", name)

if err := printer.Fprint(&buf, fset, file); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize non-API syntax before comparing

Normalize receiver and parameter identifiers before printing the snapshot. Those identifiers are not part of Go source compatibility, but the generated text includes names such as s, tx, and fn, so a harmless rename like s to store fails this test and is reported as a breaking change requiring a major version.

Useful? React with 👍 / 👎.

Comment thread api_surface_test.go
Comment on lines +130 to +132
case *ast.GenDecl:
if d.Tok == token.IMPORT {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve import identities in the API snapshot

Keep enough import information to identify qualified types. After this block removes imports, a dependency migration from one import path to another package with the same local package name leaves signatures such as pkg.Type textually unchanged, even though callers see a distinct Go type and may no longer compile; the compatibility guard therefore misses that breaking change.

Useful? React with 👍 / 👎.

Comment thread content/docs/compatibility.md Outdated
Comment on lines +35 to +37
Every flag on the [CLI Usage](../cli/) page keeps its name, its meaning, and its default,
along with the config-file key and environment variable that set it. `kvs serve` with no flags
keeps listening on `:3456` for HTTP, `:3457` for gRPC, and `127.0.0.1:6379` for RESP.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Account for the default RESP bind fallback

State this as the default address rather than promising that the process keeps listening there. When 127.0.0.1:6379 is already occupied, the branch in internal/server/run.go:80-90 deliberately treats the default RESP bind failure as nonfatal, logs that RESP is off, and continues with only HTTP and gRPC, so kvs serve with no flags does not provide the promised listener in that scenario.

Useful? React with 👍 / 👎.

@skyoo2003

Copy link
Copy Markdown
Owner Author

Codex review, six comments. Three were right about the page and are fixed in 2eab847.

  • Interoperability. Qualified to strings. internal/server/http.go:83 and internal/server/grpc.go:52 both return an internal error for a key holding a RESP list, hash, set, or sorted set, so the unqualified claim was false.
  • Authentication. Rewritten. resp_password / KVS_RESP_PASSWORD does make the RESP listener require AUTH, cluster joins included. The page now says which listener can ask who you are and which two cannot, and that nothing is encrypted either way.
  • Default RESP address. Reworded so the address is the promise rather than the bind, since internal/server/run.go:85-89 logs and skips a default RESP port that is already taken.

The other three I did not take:

  • Cluster plumbing in the golden file. It records what the package exports, not what v1 promises; removing the three methods would make the file lie about the package. The header and the failure message now point at the exemptions on the compatibility page instead.
  • Receiver and parameter names. A rename does fail this test, but it fails loudly, with the diff and the -update command in the message. Rewriting every identifier in the AST costs more than the false positive does.
  • Import identities. A real gap, but reaching it needs a swap to a different module with the same package name and the same type name, which is what go.mod review is for. Keeping the import block would instead fail the test on every unrelated import change.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2eab847f27

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md Outdated
Comment thread content/docs/compatibility.md Outdated
Comment thread content/docs/compatibility.md
Comment thread content/docs/compatibility.md
State in one place what will not break and what deliberately might, so a
reader deciding whether to depend on kvs can answer that from a page rather
than from the source.

A promise nobody checks rots, so the Go half of it is enforced:
TestPublicAPISurface renders every exported declaration in the root package
and compares it against testdata/api-surface.txt. A symbol cannot join or
leave without the diff landing in review. Comments are left unparsed and
function bodies dropped, which keeps the golden file about signatures and
free of everything else.

SetReplicator, ReplaceWith, and ApplyReplicated are exported so
internal/cluster can reach them across the package boundary, and pkg/resp
exists so the server can speak RESP2 rather than as a library for other
programs. Both now say so where someone reading godoc will find it.

The page also writes down the trust boundary as it stands: no
authentication, no authorization, no TLS, and HTTP and gRPC listening on
every interface by default while RESP stays on loopback.
The page said all three protocols read each other's values, that kvs has no
authentication at all, and that `kvs serve` keeps listening on the default RESP
address. None of the three holds: HTTP and gRPC report an error for a key
holding a RESP list, hash, set, or sorted set; `resp_password` makes the RESP
listener require AUTH before anything else, cluster joins included; and a
default RESP port already taken is logged and skipped rather than being fatal.

The golden surface keeps the exempt cluster plumbing, because it records what
the package exports rather than what v1 promises, and its header now says where
the exemptions are written down.
The HTTP API page says all errors return a JSON body and lists 405 among them,
but both method checks answered with a bare WriteHeader: no body, no JSON
content type. The compatibility page now makes those response bodies part of
v1, so this is the last release where the empty one can be corrected rather
than kept.

The 405 path had no test at all, which is how it stayed wrong.
The README repeated the claim that kvs has no authentication, which the
compatibility page had already stopped making: `resp_password` gives the RESP
listener an AUTH requirement, and only HTTP and gRPC are unauthenticated
whatever the operator does.

The command-line promise covered every flag on the CLI page, including the
config-file key and environment variable that set it. `--config`, `--version`,
and `--help` have neither, because the CLI answers them itself rather than
passing them to the server, so that half of the promise is now scoped to the
`serve` settings that do have Viper mappings. The CLI page overstated the same
thing and now says `serve` too.
@skyoo2003
skyoo2003 force-pushed the compatibility-promise branch from c9268bc to 67cc72c Compare August 10, 2026 13:46
@skyoo2003

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 67cc72c83c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/server/http.go
func (h *httpHandler) handleHealth(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exempt HEAD from the JSON error-body promise

When a client sends HEAD /healthz or HEAD /v1/keys/key, this branch selects the new 405 JSON response, but Go's net/http transport suppresses response bodies for HEAD requests. The compatibility page now promises response bodies and the HTTP API says every error has JSON, while the added test exercises only POST through httptest.ResponseRecorder, so real HEAD clients still receive an empty 405; either narrow the documented guarantee for HEAD or handle it explicitly.

Useful? React with 👍 / 👎.

Comment thread api_surface_test.go
// FileExports drops unexported declarations, struct fields, and interface methods.
// It keeps the bodies of the exported functions, which is why signaturesOnly runs
// after it: how a method is written is not what callers depend on.
if !ast.FileExports(file) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve exported structs' comparability in the snapshot

Because ast.FileExports removes unexported fields, the snapshot records ReadTx only as an opaque struct even though its current fields make the exported type comparable. If a later minor release adds an unexported slice, map, or function field, downstream code that compares ReadTx values or uses them as map keys stops compiling, but this guard produces no diff. Retain enough type information to record comparability, or type-check exported structs and include that property in the snapshot.

Useful? React with 👍 / 👎.

Comment on lines +37 to +38
Every flag on the [CLI Usage](../cli/) page keeps its name, its meaning, and its default. Each
`kvs serve` setting keeps the config-file key and environment variable that set it too;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Qualify --join or follow leader redirects

When --join points to a follower, which the CLI page's “a node already in the cluster” wording and the README's “any existing node” wording explicitly allow, that node answers MOVED, but requestJoin only calls expectOK and joinCluster retries the same follower forever instead of following the supplied leader address. The new guarantee that every flag retains its documented meaning therefore promises a join flow that does not work in this common failover scenario; either follow MOVED or restrict the promised target to the current leader.

Useful? React with 👍 / 👎.

@skyoo2003
skyoo2003 merged commit fe4cb4f into main Aug 10, 2026
6 checks passed
@skyoo2003
skyoo2003 deleted the compatibility-promise branch August 10, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation go Pull requests that update Go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant