# Instrumented-session SDK — upgrade guide

**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:

1. `GET /api/study/connectors/protocol` — the envelope-version window this
   workspace decodes. The SDK's `negotiateProtocol` compares it against
   `SDK_PROTOCOL_VERSION` and **refuses to send** outside it. A refused upload
   is recoverable; a mis-decoded session is not.
2. `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`),
- `sequenced` going `false` → `true` (strengthening),
- `privacyClass` tightening,
- a new stream (under `additive-only` or `version-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,
  provided `compatibility.migrationNotesRef` points 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:

```ts
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:

1. **handshake** — every stream you emit is declared, and the handshake and
   envelope name the same connector.
2. **envelope** — parses, sits inside its own compatibility window, manifest
   covers every stream.
3. **fixtures** — every stream passes the nine per-family dimensions.
4. **clock-drift** — sync points fit within the drift and error bounds.
5. **backwards-compatibility** — the registry is a legal successor under your
   own declared policy.
6. **privacy** — nothing emitted falls outside the ingest plan.

## 7. Upgrade checklist

- [ ] `negotiateProtocol` accepts the workspace's advertised window.
- [ ] The release is published to `POST /connectors/registry` and admitted.
- [ ] Every changed stream has an advanced `schemaVersion`.
- [ ] Every breaking change has a major bump, `version-negotiated`
      compatibility, and `migrationNotesRef` pointing 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.
- [ ] `runSdkConformance` reports `conformant: true`.
