The problem
Four divergences between the Go SDK and the Rust reference. Two of them make an
ordinary Go provider unusable against the reference host; none of them is
visible to CI, because the one example provider the sdk-go job runs happens to
avoid all four.
1. An empty result serialises as "frames": null, which the reference host rejects
sdk/go/contextgraph/types.go:
type ContextQueryResult struct {
Frames []ContextFrame `json:"frames"`
Truncated bool `json:"truncated"`
DroppedEstimate *uint32 `json:"dropped_estimate,omitempty"`
}
encoding/json marshals a nil slice to null, not []. Proved:
$ go run … // same struct shape
nil slice marshals to: {"frames":null,"truncated":false}
Nothing in provider.go's handleLine or http.go's RespondToBody
normalises it. On the Rust side, contextgraph-types/src/query.rs declares:
pub struct ContextQueryResult {
pub frames: Vec<ContextFrame>,
with no #[serde(default)] and no custom deserializer, so "frames": null is a
deserialisation error rather than an empty result.
The trigger is the most ordinary case there is: a provider that finds no matches
and returns cg.ContextQueryResult{}, or builds var frames []cg.ContextFrame
and never appends. "I have nothing relevant" is an explicitly permitted answer —
check_frames in contextgraph-conformance/src/lib.rs returns pass with
"provider returned 0 frames (permitted — nothing relevant to the probe)" — and
in Go it is the one answer that cannot be delivered.
The same hazard applies to VerifyResponse.Verdicts for a user-supplied
Verifier. The SDK's own no-verify fallback is safe because it uses
make([]FrameVerdict, len(...)).
2. errors.As silently drops the error code for the idiomatic Go spelling
sdk/go/contextgraph/provider.go's handleLine, and the same block in
http.go's handleEnvelope:
reply := errorReply{Type: "error", Message: err.Error(), ID: envelope.ID}
var pe ProviderError
if errors.As(err, &pe) {
reply.Code = pe.Code
}
ProviderError.Error() has a value receiver, so both ProviderError and
*ProviderError satisfy error and both compile as a Query return. But
errors.As with a *ProviderError target only matches a value. Proved:
errors.As(value): true code: bad_request
errors.As(pointer): false code: ""
return nil, &cg.ProviderError{Code: "bad_request", …} — the spelling most Go
authors reach for, and the one go vet will not question — produces a codeless
error envelope. check_embedding_fingerprint reads that as "not
bad_request" and fails §E1. The author sees a conformance failure with no
indication that the & is the cause.
3. ContextFrame has no embedding field at all
The Go ContextFrame carries every other frame field — content_ref,
transform, minimum_content_fidelity, canonical_token_cost,
tokenizer_ref, relations, and the rest — and has no Embedding. There is no
FrameEmbedding type in the package.
contextgraph-types/src/frame.rs has pub embedding: Option<FrameEmbedding>,
schema/contextgraph-envelope.schema.json defines FrameEmbedding and puts it
on the frame, and both the TypeScript and Python SDKs expose it. So a Go
provider can declare Capabilities.EmbeddingsFingerprint — the bundled example
does — and can never attach a vector to a frame it returns.
4. Go's §E1 check accepts a present-but-empty embedding the other three reject
sdk/go/examples/example-docs/main.go:
if n := len(query.Embedding); n > 0 && n != embeddingDimensions {
Rust's embedding_dimension_error treats Some(vec![]) as 0 ≠ 384 and rejects
it. TypeScript uses embedding !== undefined && embedding.length !== …; Python
uses embedding is not None and len(embedding) != …. Only Go lets an empty
vector through.
The root cause is structural rather than a slip: ContextQuery.Embedding []float64 with omitempty cannot distinguish absent from empty, so a Go
provider has no way to implement the rule as the other three do. Worth deciding
whether the fix is a *[]float64, or whether §E1 should be reworded so an empty
vector and an absent one mean the same thing everywhere.
Why CI does not see any of it
.github/workflows/ci.yml's sdk-go and sdk-go-http jobs run
sdk/go/examples/example-docs, which returns two hard-coded frames on every
query (never empty), returns a value ProviderError (never a pointer), declares
an embedding fingerprint it never attaches a vector for, and is probed with a
384-length embedding (never an empty one). Each defect sits exactly outside the
one path the example exercises.
What I verified vs. inferred
Verified by executing Go: the nil-slice-to-null marshalling and the
errors.As value-vs-pointer miss, both reproduced with a standalone program
using the same type shapes and the same value-receiver Error() method.
Verified by reading, on origin/main at a01ca64: the four Go source sites
quoted; ProviderError's value receiver in sdk/go/contextgraph/provider.go;
the absence of any embedding field in Go's ContextFrame; the Rust
ContextQueryResult declaration; the three languages' §E1 predicates; and the
sdk-go / sdk-go-http job definitions.
Inferred, not verified: that serde rejects "frames": null for a
Vec<ContextFrame> without #[serde(default)]. That is serde's documented
behaviour for a sequence type, but I did not run the round-trip. A fixer should
confirm it with one test before deciding whether item 1 needs a Go-side fix, a
Rust-side #[serde(default)], or both.
What "done" looks like
- A Go provider returning no frames produces
"frames": []. The cheapest
durable fix is for handleLine and RespondToBody to normalise a nil
Frames (and Verdicts) to an empty slice before marshalling, so no provider
author has to remember. Whether the Rust side should also accept null
defensively is a separate call — accepting it is more forgiving, rejecting it
keeps the wire unambiguous.
- The
code survives a *ProviderError. Either give Error() a pointer
receiver and match on *ProviderError, or check both targets. Document which
spelling is supported in the type's doc comment, because right now nothing
tells an author the & matters.
- Go's
ContextFrame carries embedding, or the SDK's README says plainly that
it does not and why.
- The empty-vector §E1 divergence is resolved in one direction, in the SPEC or
in the SDK, so all four implementations agree.
- Witnesses: a Go test that marshals an empty
ContextQueryResult and asserts
"frames":[]; a Go test that returns &ProviderError{Code: "bad_request"}
and asserts the reply carries the code. Both belong in sdk/go's own tests,
not only in the conformance run.
- Consider making the bundled Go example return zero frames for a query it
cannot serve, so the conformance run exercises the empty case at all.
Constraints
The problem
Four divergences between the Go SDK and the Rust reference. Two of them make an
ordinary Go provider unusable against the reference host; none of them is
visible to CI, because the one example provider the
sdk-gojob runs happens toavoid all four.
1. An empty result serialises as
"frames": null, which the reference host rejectssdk/go/contextgraph/types.go:encoding/jsonmarshals a nil slice tonull, not[]. Proved:Nothing in
provider.go'shandleLineorhttp.go'sRespondToBodynormalises it. On the Rust side,
contextgraph-types/src/query.rsdeclares:with no
#[serde(default)]and no custom deserializer, so"frames": nullis adeserialisation error rather than an empty result.
The trigger is the most ordinary case there is: a provider that finds no matches
and returns
cg.ContextQueryResult{}, or buildsvar frames []cg.ContextFrameand never appends. "I have nothing relevant" is an explicitly permitted answer —
check_framesincontextgraph-conformance/src/lib.rsreturns pass with"provider returned 0 frames (permitted — nothing relevant to the probe)" — and
in Go it is the one answer that cannot be delivered.
The same hazard applies to
VerifyResponse.Verdictsfor a user-suppliedVerifier. The SDK's own no-verify fallback is safe because it usesmake([]FrameVerdict, len(...)).2.
errors.Assilently drops the error code for the idiomatic Go spellingsdk/go/contextgraph/provider.go'shandleLine, and the same block inhttp.go'shandleEnvelope:ProviderError.Error()has a value receiver, so bothProviderErrorand*ProviderErrorsatisfyerrorand both compile as aQueryreturn. Buterrors.Aswith a*ProviderErrortarget only matches a value. Proved:return nil, &cg.ProviderError{Code: "bad_request", …}— the spelling most Goauthors reach for, and the one
go vetwill not question — produces a codelesserrorenvelope.check_embedding_fingerprintreads that as "notbad_request" and fails §E1. The author sees a conformance failure with noindication that the
&is the cause.3.
ContextFramehas noembeddingfield at allThe Go
ContextFramecarries every other frame field —content_ref,transform,minimum_content_fidelity,canonical_token_cost,tokenizer_ref,relations, and the rest — and has noEmbedding. There is noFrameEmbeddingtype in the package.contextgraph-types/src/frame.rshaspub embedding: Option<FrameEmbedding>,schema/contextgraph-envelope.schema.jsondefinesFrameEmbeddingand puts iton the frame, and both the TypeScript and Python SDKs expose it. So a Go
provider can declare
Capabilities.EmbeddingsFingerprint— the bundled exampledoes — and can never attach a vector to a frame it returns.
4. Go's §E1 check accepts a present-but-empty embedding the other three reject
sdk/go/examples/example-docs/main.go:Rust's
embedding_dimension_errortreatsSome(vec![])as 0 ≠ 384 and rejectsit. TypeScript uses
embedding !== undefined && embedding.length !== …; Pythonuses
embedding is not None and len(embedding) != …. Only Go lets an emptyvector through.
The root cause is structural rather than a slip:
ContextQuery.Embedding []float64withomitemptycannot distinguish absent from empty, so a Goprovider has no way to implement the rule as the other three do. Worth deciding
whether the fix is a
*[]float64, or whether §E1 should be reworded so an emptyvector and an absent one mean the same thing everywhere.
Why CI does not see any of it
.github/workflows/ci.yml'ssdk-goandsdk-go-httpjobs runsdk/go/examples/example-docs, which returns two hard-coded frames on everyquery (never empty), returns a value
ProviderError(never a pointer), declaresan embedding fingerprint it never attaches a vector for, and is probed with a
384-length embedding (never an empty one). Each defect sits exactly outside the
one path the example exercises.
What I verified vs. inferred
Verified by executing Go: the nil-slice-to-
nullmarshalling and theerrors.Asvalue-vs-pointer miss, both reproduced with a standalone programusing the same type shapes and the same value-receiver
Error()method.Verified by reading, on
origin/mainat a01ca64: the four Go source sitesquoted;
ProviderError's value receiver insdk/go/contextgraph/provider.go;the absence of any embedding field in Go's
ContextFrame; the RustContextQueryResultdeclaration; the three languages' §E1 predicates; and thesdk-go/sdk-go-httpjob definitions.Inferred, not verified: that serde rejects
"frames": nullfor aVec<ContextFrame>without#[serde(default)]. That is serde's documentedbehaviour for a sequence type, but I did not run the round-trip. A fixer should
confirm it with one test before deciding whether item 1 needs a Go-side fix, a
Rust-side
#[serde(default)], or both.What "done" looks like
"frames": []. The cheapestdurable fix is for
handleLineandRespondToBodyto normalise a nilFrames(andVerdicts) to an empty slice before marshalling, so no providerauthor has to remember. Whether the Rust side should also accept
nulldefensively is a separate call — accepting it is more forgiving, rejecting it
keeps the wire unambiguous.
codesurvives a*ProviderError. Either giveError()a pointerreceiver and match on
*ProviderError, or check both targets. Document whichspelling is supported in the type's doc comment, because right now nothing
tells an author the
&matters.ContextFramecarriesembedding, or the SDK's README says plainly thatit does not and why.
in the SDK, so all four implementations agree.
ContextQueryResultand asserts"frames":[]; a Go test that returns&ProviderError{Code: "bad_request"}and asserts the reply carries the code. Both belong in
sdk/go's own tests,not only in the conformance run.
cannot serve, so the conformance run exercises the empty case at all.
Constraints
ContextFrameis not a breaking change for existingGo providers; changing
ProviderError.Error()'s receiver could be, for anyonewho stores a
ProviderErrorby value in an interface. Checksdk/go'sreleased versions before choosing.
typechecking. This issue is about Go runtime behaviour and overlaps neither.
ProviderErrorthrow is swallowed and the host is left waiting) and the Python SDK has its own
(The Python SDK provider dies on two kinds of message the spec says it must survive #146). Three SDKs, three different failures on the same rule, which is worth
noting when deciding whether the conformance probe set is the real gap.