# `@yemaya/instrumented-session-sdk`

The versioned instrumented-session SDK an owned game embeds to deliver a
Tier B/C study session (proposal §12.2, YSD-12020/12024/12027/12028; shipped
under YSD-19082).

It depends on `@oshun/contracts` and nothing else in the monorepo. A game does
not take a dependency on the study workspace in order to send it a session.

## What it does that a hand-rolled uploader does not

| Concern                | What the SDK does                                                                                                |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------- |
| Sequences              | The recorder owns them, per stream, monotonic. A caller that could set one could fake packet loss.                 |
| Instants               | Stamped on the stream's **declared** clock basis. A stream on the engine frame or video clock must supply its own. |
| A full buffer          | Drops the **oldest** and counts it, so the retained window stays contiguous and the gap check reads truthfully.    |
| Loss                   | Every drop, rate refusal and backwards clock step is in the capture report — **inside the signed hash**.           |
| Signing                | The SDK computes the hash and hands it to the host's key. It never manufactures a signature.                       |
| Protocol drift         | Negotiates against the workspace's advertised window and **refuses to send** outside it.                           |
| Partial delivery       | Acknowledges only what the receiver counted; the tail stays for a retry.                                           |
| An unclassified field  | Fails the build of the ingest plan, naming the paths. Nothing defaults to `gameplay`.                              |

## Shape of an integration

```ts
import {
  buildEnvelope,
  buildIngestPlan,
  createHttpTransport,
  createSessionRecorder,
  flush,
} from '@yemaya/instrumented-session-sdk';

const recorder = createSessionRecorder({
  sessionId,
  producerBuild: '1.4.2+9f21c',
  captureClass: 'deterministic-replay',
  now: () => performance.now(),
  streams: [
    {
      stream: 'input',
      family: 'input',
      schemaRef: 'yemaya/study/events/input',
      schemaVersion: '1.0.0',
      ownerDomain: 'yemaya',
      clockBasis: 'session-monotonic',
      sequenced: true,
      privacyClass: 'pseudonymized',
      samplingInterval: 100,
    },
  ],
});

recorder.emit('input', { family: 'input', action: 'attack-light' /* … */ });
recorder.addSyncPoint('engine-frame', currentFrameIndex);

const built = await buildEnvelope(recorder, {
  engine: {
    engineFamily: 'v2',
    engineVersion,
    connectorId,
    connectorVersion,
  },
  maxSupportedTier: 'instrumented-session',
  supportedCaptureClasses: ['deterministic-replay'],
  negotiatedAt: handshakeInstant,
  sessionAuthorityGrantId: grantId,
  sign: (contentHash) => hostKey.sign(contentHash),
});

await flush(recorder, createHttpTransport({ baseUrl, projectId, editionId, territory, fetch }));
```

## Proving a connector before it ships

Run the workspace's conformance suite against a **real** capture — not a
hand-written fixture — by adapting the recorder:

```ts
import { asConformanceSubject } from '@yemaya/instrumented-session-sdk';
import { runSdkConformance } from '@yemaya/study-workspace';

const report = runSdkConformance(
  asConformanceSubject(recorder, { built, ingestPlan, engineFrameRateHz: 60 }),
  { pseudonymize }
);
```

Six checks, no partial credit. The rules the checks enforce, and what counts as
a breaking registry change, are in
[`docs/integrations/instrumented-session-sdk/UPGRADE.md`](../../../docs/integrations/instrumented-session-sdk/UPGRADE.md).

The first-party reference integration is V2 (decision
[YSD-0127](../../../docs/proposals/yemaya-study-workspace/decisions/ysd-0127.md)):
`libs/aphrodite/game-runtime/src/study-connector`.
