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
- 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.
- Anything breaking should land in one release, not dribble out — these are published packages.
- Editing code without editing
tests/parity/manifest.json will fail the suite in all four languages, loudly and by name. That is intended.
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.
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.mdcited 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_strwas 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 hasverifyChecksums. Go and Rust have no such registry option at all. Whether an artifact is re-hashed beforedlopenis 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 RegistryErrorwhile the other five artifact errorsextend FetchError, so a TS caller must catch two unrelated types to handle all six conditions. Python hasArtifactError, Go has*ArtifactErrorwith.Code, Rust hasError::artifact_code().Recommendation: reparent under one
ArtifactError, or export a predicate mirroring Rust'sartifact_code(). Prototype-chain surgery on published classes is a real decision.A4. Python's
libraries()is the only one with a side effectIt 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
ReasonStringPadholds"fixedstring_pad"The value is identical across all four and is checked by the parity suite. Only Go's identifier names a different concept —
Stringwhere the others sayFixedString. A rename is breaking.Recommendation: leave until the next breaking window, then fix.
A6.
Libraryis the only lifetime object without scope-based releasePython:
Registry/Schema/Filter/Blockare context managers,Libraryis not. TypeScript: the same four haveSymbol.dispose,Librarydoes not. Sowith 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 aLibraryis 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.jsonwithNEEDS A DECISIONin the absence reason, so they are greppable.registry.search-path— missing in go, present in python/ts/rustThe 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/rustPython 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/rustTS 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/rustTS'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/tsThe 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/rustGo 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/rustThe 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 asfetch.env-registry.Cheapest sweep if you want them gone: Go and TS export
CHTYPES_REGISTRY/CHTYPES_AUTOFETCH; TS exportsRELEASE_KEY_IDandDEFAULT_LOCK_FILE; Rust exportsLOCK_SCHEMA; Python exports aFETCH_COMMANDand 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
*Libraryhas noSetDefaultSettingsand no teardown. This is structural and load-bearing: the function-pointer table deliberately does notdlsymchs_set_default_settingsorchs_shutdown, so aLibrarystructurally 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
Dropinstead ofclose()(the borrow checker enforces free-before-schema at compile time); Rust'srows_export(Option<Format>)whereNoneisCHS_EXPORT_NONE; and therow/row_with_settingstwins in Go and Rust versus optional arguments in Python and TypeScript, which §The object model already blesses.How to work this
tests/parity/manifest.jsonwill fail the suite in all four languages, loudly and by name. That is intended.tests/parity/manifest.jsonandgo/chtypes/testdata/parity.jsonmust stay byte-identical — Go embeds its copy so the check survivesscripts/check-standalone.sh's bare-copy tar. Three suites will tell you if you forget.