Scope: engine connectors that deliver instrumented gameplay sessions to the Yemaya study workspace (proposal §12.2, YSD-12020/12024/12027/12028).
Everything a study claims about a game rests on what a connector sent. This guide covers what may change between connector releases, what may not, and how to prove a release is safe before it ships.
Reference material:
- The SDK you embed —
libs/yemaya/instrumented-session-sdk(@yemaya/instrumented-session-sdk) - Envelope contract —
libs/contracts/src/study/instrumented-session-envelope.ts - Event families —
libs/contracts/src/study/session-event-families.ts - Registry rules —
libs/yemaya/study-workspace/src/policies/event-schema-registry.ts - Registry record —
libs/contracts/src/study/entities/connector-registry.ts - Conformance suite —
libs/yemaya/study-workspace/src/testing/instrumented-sdk-conformance.ts - First-party reference integration (V2, decision YSD-0127) —
libs/aphrodite/game-runtime/src/study-connector - Minimal shape-only connector —
libs/yemaya/study-workspace/src/testing/sample-instrumented-connector.ts
0. Before you send anything#
Two calls, in this order, every release:
GET /api/study/connectors/protocol— the envelope-version window this workspace decodes. The SDK'snegotiateProtocolcompares it againstSDK_PROTOCOL_VERSIONand refuses to send outside it. A refused upload is recoverable; a mis-decoded session is not.POST /api/study/connectors/registry— publish the streams this release declares.
The registry is stored, one live release per connector per tenant, revised rather than edited. You do not supply the release you supersede: the workspace reads it. That is the difference between a compatibility check and a compatibility ritual — a connector that could name its own predecessor could always name a flattering one.
Sessions then pin your connectorId at registration, and every batch they carry
is checked against what you published. A connector that has published nothing
leaves the cross-version check reporting itself unrun, which is honest, and
is not the same as a pass.
1. The one rule that has no exceptions#
A stream's declaration may not change while its version stays the same.
Consumers cache decoders by schemaVersion. A stream that changes shape,
ownership, clock basis, sequencing guarantee, or privacy class under an
unchanged version silently mis-decodes on every consumer that has already seen
it. checkRegistryUpgrade refuses this in every compatibility mode — there is
no configuration flag that permits it.
If you changed a stream, advance its version. If you are not sure whether you changed it, run the suite; it will tell you.
2. What counts as breaking#
A change is breaking — requiring a major version bump, and only permitted
under version-negotiated compatibility with migration notes on record — when
it does any of:
| Change | Why it breaks |
|---|---|
schemaRef moves |
the shape moved; old readers decode the wrong fields |
ownerDomain moves |
the domain accountable for the semantics changed |
clockBasis moves |
every alignment built on the old basis silently shifts |
sequenced goes true → false |
consumers that ordered on it keep working and order wrongly |
privacyClass loosens |
data collected under one promise is handled under a weaker one |
Loosening a privacy class additionally requires an explicit relaxation
authorization naming the stream, and moving ownerDomain requires an ownership
transfer on record. Neither is inferred from the diff.
Compatible changes still need a version bump, just not a major one:
- a new producing build (
producerBuild), sequencedgoingfalse→true(strengthening),privacyClasstightening,- a new stream (under
additive-onlyorversion-negotiated).
3. Choosing a compatibility mode#
Declared in the envelope's compatibility.eventSchemaCompatMode:
strict— only the producing build may change. Nothing is added, nothing is removed. Use while a study is mid-flight and its results must stay comparable.additive-only— new streams and strengthening revisions are admitted; removals and breaking revisions are not. The normal mode for a shipping connector.version-negotiated— breaking changes and removals are admitted, providedcompatibility.migrationNotesRefpoints at notes consumers can negotiate against. Use for a major connector release.
Removing a stream additionally requires a retirement on record, in any mode.
4. Clocks#
A connector that stamps events on any clock other than session-monotonic
must supply sync points for that clock. Without them the workspace cannot
place the events on the session timeline at all, and the conformance suite fails
the connector rather than guessing.
Bounds the suite enforces by default:
- drift: ±500 ppm — a crystal drifting further cannot hold a frame over a minute;
- worst-case sync residual: 8 ms — half a frame at 60 Hz, the bound a per-frame claim needs.
Both are overridable per run for hardware with known-worse characteristics, but the loosened bound then travels with the study, not with the connector.
Three sync points is the floor. Two fit a line exactly and leave no residual to estimate error from — the workspace reports such a span as underdetermined rather than as a perfect fit.
5. Privacy#
The ingest plan is an allowlist. A field the plan does not classify is held
back, not stored. That inversion is deliberate: a connector upgrade that starts
emitting player.email must not have its new field flow through on the strength
of nobody having forbidden it yet.
Practical consequence for an upgrade: any new field is a plan change. Add the classification in the same release that adds the field, or the field is quarantined and the connector fails its privacy check.
Classes that cannot be pseudonymized into acceptability: chat content and voice content. A pseudonymized chat line is still a chat line; a voice is its own identifier. Both are dropped.
6. Before you ship#
Run the suite:
import { runSdkConformance } from '@oshun/yemaya-study-workspace';
const report = runSdkConformance(
myConnector,
{ pseudonymize },
{ maxDriftPpm: 500 }
);
if (!report.conformant) {
for (const check of report.results.filter((r) => !r.passed)) {
console.error(check.check, check.issues);
}
}
Six checks, all of which must pass — there is no partial credit, because a study cannot use half a session:
- handshake — every stream you emit is declared, and the handshake and envelope name the same connector.
- envelope — parses, sits inside its own compatibility window, manifest covers every stream.
- fixtures — every stream passes the nine per-family dimensions.
- clock-drift — sync points fit within the drift and error bounds.
- backwards-compatibility — the registry is a legal successor under your own declared policy.
- privacy — nothing emitted falls outside the ingest plan.
7. Upgrade checklist#
-
negotiateProtocolaccepts the workspace's advertised window. - The release is published to
POST /connectors/registryand admitted. - Every changed stream has an advanced
schemaVersion. - Every breaking change has a major bump,
version-negotiatedcompatibility, andmigrationNotesRefpointing at this file or its successor. - Every privacy relaxation has an authorization naming the stream.
- Every ownership move has a transfer on record naming the receiving domain.
- Every removed stream has a retirement on record.
- Every new field appears in the ingest plan in the same release.
- Sync points exist for every non-session clock basis you stamp on.
-
runSdkConformancereportsconformant: true.