Skip to content

SDK parity: 12 divergences between the four bindings that need a decision, not a fix #13

Description

@EricAndrechek

Decision needed from Eric — nothing here is a defect to fix; each item is a judgement call, and Group B is a single yes/no covering all eight additive exports.

The four bindings are now checked for API parity mechanically — tests/parity/manifest.json, 157 capabilities × 4 languages, enforced by each language's own suite. That work is done and green. This issue is the backlog it exposed: divergences that are real, recorded, and deliberately not resolved, because each is a judgement call and several are breaking changes to published packages.

Read this first, because a passing test here does not mean what it looks like. The parity suite proves the bindings match the manifest — and the manifest currently records these divergences as known. Green means "the divergences are exactly the ones we wrote down," not "the bindings are equivalent." Closing an item means editing the manifest entry as well as the code, or the suite will fail.

Why this went unnoticed until now

Worth stating, because it shapes how to think about the list. Every test in this repo asked "does this binding work?" The only cross-binding test was the golden set, and it asks whether the four give the same answers, not whether they expose the same surface. Four bindings can pass all 32 golden cases identically while having completely different APIs — which is what happened. The contract existed as a 60-row table in docs/reference/bindings.md cited by comments in 33 source files, and prose does not fail.


Group A — shape and behavior differences (higher value)

All four bindings have these capabilities; they differ in what they do. None appears in the manifest as an absence, so the parity suite does not catch them.

A1. The filter verdict vocabulary genuinely diverges

Go and Python render 't' / 'f' / 'e' / 'd' — the wire character from the C document. TypeScript renders 'true' / 'false' / 'error' / 'decline'. Rust renders nothing (fixed separately: FilterOutcome::as_str was added).

The four-state concept and the fail-closed semantics are identical everywhere, so this is not a safety bug. But two SDKs cannot share a log format or a test fixture, and this is the surface docs/reference/c-abi.md §Revision 3 calls "the measured leak class."

Recommendation: align TypeScript onto the wire characters. Breaking change to a published type union. Cheapest now — crates.io shows 9 downloads all-time.

A2. Load-time checksum verification is 2-of-4

Python has verify_hashes, TypeScript has verifyChecksums. Go and Rust have no such registry option at all. Whether an artifact is re-hashed before dlopen is a security posture, not an API spelling — a Go consumer cannot opt into what a Python consumer gets.

Recommendation: add it to Go and Rust. Behavioral, so it was left alone.

A3. TypeScript has no single catchable artifact error type

ArtifactMissingError extends RegistryError while the other five artifact errors extend FetchError, so a TS caller must catch two unrelated types to handle all six conditions. Python has ArtifactError, Go has *ArtifactError with .Code, Rust has Error::artifact_code().

Recommendation: reparent under one ArtifactError, or export a predicate mirroring Rust's artifact_code(). Prototype-chain surgery on published classes is a real decision.

A4. Python's libraries() is the only one with a side effect

It eagerly _load()s every lazily-discovered line — roughly 120 MB of artifact per line — as the side effect of a listing call. Go, TypeScript and Rust list what is already loaded.

Recommendation: make Python match. Behavioral.

A5. Go's ReasonStringPad holds "fixedstring_pad"

The value is identical across all four and is checked by the parity suite. Only Go's identifier names a different concept — String where the others say FixedString. A rename is breaking.

Recommendation: leave until the next breaking window, then fix.

A6. Library is the only lifetime object without scope-based release

Python: Registry/Schema/Filter/Block are context managers, Library is not. TypeScript: the same four have Symbol.dispose, Library does not. So with registry.for_version("25.8") as lib: reads as though it should work, and raises.

This one is probably already correct and just needs confirming. docs/reference/bindings.md §Teardown measured that reopening after a full close segfaults, so a scope-based close on a Library is precisely the mid-lifecycle teardown that section warns against. If you agree, the action is to document it, not change it.


Group B — constant-export raggedness (lower value, mechanical)

Each of these is a caller retyping a literal that the other bindings hand them. None is a correctness risk. All are recorded in tests/parity/manifest.json with NEEDS A DECISION in the absence reason, so they are greppable.

registry.search-path — missing in go, present in python/ts/rust

The Go registry keeps its search path unexported and renders it into the ErrArtifactMissing message instead.
NEEDS A DECISION: the other three expose it as an accessor, so a Go caller cannot print the path it is about
to be told is empty.

fetch.parse-signature-file — missing in python, present in go/ts/rust

Python keeps signature parsing private (_parse_signature) and exposes only verify_sums_signature. NEEDS A
DECISION — Go, TS and Rust export the splitter.

fetch.release-key-id — missing in ts, present in go/python/rust

TS carries RELEASE_PUBLIC_KEYS (a list, for rotation) and derives the id with keyId(); it exports no
precomputed id constant. NEEDS A DECISION: the other three export one.

fetch.default-lock-file — missing in ts, present in go/python/rust

TS's readLock takes the path explicitly and its CLI spells the default inline; it exports no constant. NEEDS A
DECISION — the other three export one.

fetch.lock-schema — missing in rust, present in go/python/ts

The Rust lock reader validates the schema number inside LockFile rather than exporting it. NEEDS A DECISION —
the other three export it.

fetch.env-registry — missing in go, present in python/rust

Go spells the name inline in RegistrySearchPath and exports no constant for it. NEEDS A DECISION — the other
three export one.

fetch.command — missing in python, present in go/ts/rust

The Python message builds the command inline in errors.py's ArtifactMissingError. NEEDS A DECISION — Go, TS
and Rust export it.

Plus fetch.env-autofetch, the same shape as fetch.env-registry.

Cheapest sweep if you want them gone: Go and TS export CHTYPES_REGISTRY/CHTYPES_AUTOFETCH; TS exports RELEASE_KEY_ID and DEFAULT_LOCK_FILE; Rust exports LOCK_SCHEMA; Python exports a FETCH_COMMAND and a public signature-file splitter.


Deliberate divergences — do NOT "fix" these

Recorded in the manifest with their reasoning so nobody closes them by accident. The most important:

Go's dlopen'd *Library has no SetDefaultSettings and no teardown. This is structural and load-bearing: the function-pointer table deliberately does not dlsym chs_set_default_settings or chs_shutdown, so a Library structurally cannot reach them. Omitting the symbol is what buys that guarantee; adding it would remove the thing the design exists for. It exists package-level under -tags chtypes_linked, guarded by a mutex.

Also deliberate: Rust's Drop instead of close() (the borrow checker enforces free-before-schema at compile time); Rust's rows_export(Option<Format>) where None is CHS_EXPORT_NONE; and the row/row_with_settings twins in Go and Rust versus optional arguments in Python and TypeScript, which §The object model already blesses.

How to work this

  1. Decide per item. A1 and A2 are the two worth doing first; A2 is a security-posture gap and A1 gets cheaper to fix never.
  2. Anything breaking should land in one release, not dribble out — these are published packages.
  3. Editing code without editing tests/parity/manifest.json will fail the suite in all four languages, loudly and by name. That is intended.
  4. tests/parity/manifest.json and go/chtypes/testdata/parity.json must stay byte-identical — Go embeds its copy so the check survives scripts/check-standalone.sh's bare-copy tar. Three suites will tell you if you forget.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions