@seriousgaming/sdk 0.0.0-stage → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +1 -0
  3. package/README.md +29 -2
  4. package/lib/build/callbacks.d.ts +998 -0
  5. package/lib/build/callbacks.js +32 -0
  6. package/lib/build/context-merge.d.ts +59 -0
  7. package/lib/build/context-merge.js +118 -0
  8. package/lib/build/document.d.ts +36 -0
  9. package/lib/build/document.js +8 -0
  10. package/lib/build/encoding.d.ts +26 -0
  11. package/lib/build/encoding.js +78 -0
  12. package/lib/build/endpoints.d.ts +421 -0
  13. package/lib/build/endpoints.js +46 -0
  14. package/lib/build/hooks.d.ts +74 -0
  15. package/lib/build/hooks.js +1 -0
  16. package/lib/build/index.d.ts +18 -0
  17. package/lib/build/index.js +16 -0
  18. package/lib/build/llm.d.ts +166 -0
  19. package/lib/build/llm.js +17 -0
  20. package/lib/build/resource-schema.d.ts +1048 -0
  21. package/lib/build/resource-schema.js +7 -0
  22. package/lib/build/scheduling.d.ts +112 -0
  23. package/lib/build/scheduling.js +217 -0
  24. package/lib/build/seed.d.ts +51 -0
  25. package/lib/build/seed.js +89 -0
  26. package/lib/build/signatures.d.ts +372 -0
  27. package/lib/build/signatures.js +1 -0
  28. package/lib/build/speech.d.ts +102 -0
  29. package/lib/build/speech.js +11 -0
  30. package/lib/build/tick-validation.d.ts +23 -0
  31. package/lib/build/tick-validation.js +96 -0
  32. package/lib/build/timer.d.ts +74 -0
  33. package/lib/build/timer.js +1 -0
  34. package/lib/build/validators.d.ts +116 -0
  35. package/lib/build/validators.js +1 -0
  36. package/lib/commands.d.ts +357 -0
  37. package/lib/commands.js +14 -0
  38. package/lib/e2e.d.ts +238 -0
  39. package/lib/e2e.js +248 -0
  40. package/lib/projection.d.ts +266 -0
  41. package/lib/projection.js +23 -0
  42. package/lib/testing/index.d.ts +13 -0
  43. package/lib/testing/index.js +13 -0
  44. package/lib/testing/logic-args.d.ts +177 -0
  45. package/lib/testing/logic-args.js +229 -0
  46. package/lib/testing/stub-services.d.ts +41 -0
  47. package/lib/testing/stub-services.js +176 -0
  48. package/lib/ui/CrossTeamDataContext.d.ts +36 -0
  49. package/lib/ui/CrossTeamDataContext.js +32 -0
  50. package/lib/ui/PresentationContext.d.ts +72 -0
  51. package/lib/ui/PresentationContext.js +13 -0
  52. package/lib/ui/PresentationWrapper.d.ts +12 -0
  53. package/lib/ui/PresentationWrapper.js +78 -0
  54. package/lib/ui/SessionUserInterfaceCallbacksContext.d.ts +3 -0
  55. package/lib/ui/SessionUserInterfaceCallbacksContext.js +7 -0
  56. package/lib/ui/SessionUserInterfaceDataContext.d.ts +103 -0
  57. package/lib/ui/SessionUserInterfaceDataContext.js +17 -0
  58. package/lib/ui/SessionUserInterfaceHookFailuresContext.d.ts +44 -0
  59. package/lib/ui/SessionUserInterfaceHookFailuresContext.js +15 -0
  60. package/lib/ui/SessionUserInterfaceHostOverlaysContext.d.ts +23 -0
  61. package/lib/ui/SessionUserInterfaceHostOverlaysContext.js +23 -0
  62. package/lib/ui/SessionUserInterfaceLocaleContext.d.ts +11 -0
  63. package/lib/ui/SessionUserInterfaceLocaleContext.js +13 -0
  64. package/lib/ui/SessionUserInterfaceMetaContext.d.ts +39 -0
  65. package/lib/ui/SessionUserInterfaceMetaContext.js +13 -0
  66. package/lib/ui/SessionUserInterfaceParticipantsContext.d.ts +12 -0
  67. package/lib/ui/SessionUserInterfaceParticipantsContext.js +16 -0
  68. package/lib/ui/SessionUserInterfaceTeamConfigurationContext.d.ts +53 -0
  69. package/lib/ui/SessionUserInterfaceTeamConfigurationContext.js +23 -0
  70. package/lib/ui/SessionUserInterfaceTeamLinksContext.d.ts +56 -0
  71. package/lib/ui/SessionUserInterfaceTeamLinksContext.js +12 -0
  72. package/lib/ui/SessionUserInterfaceTeamsContext.d.ts +13 -0
  73. package/lib/ui/SessionUserInterfaceTeamsContext.js +13 -0
  74. package/lib/ui/SessionUserInterfaceThemeContext.d.ts +9 -0
  75. package/lib/ui/SessionUserInterfaceThemeContext.js +7 -0
  76. package/lib/ui/SessionUserInterfaceThemePreferenceContext.d.ts +3 -0
  77. package/lib/ui/SessionUserInterfaceThemePreferenceContext.js +7 -0
  78. package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.d.ts +42 -0
  79. package/lib/ui/SessionUserInterfaceWalkthroughLinkContext.js +26 -0
  80. package/lib/ui/SessionUserInterfaceWrapper.d.ts +8 -0
  81. package/lib/ui/SessionUserInterfaceWrapper.js +321 -0
  82. package/lib/ui/SettingsEditorCallbacksContext.d.ts +3 -0
  83. package/lib/ui/SettingsEditorCallbacksContext.js +7 -0
  84. package/lib/ui/SettingsEditorContext.d.ts +33 -0
  85. package/lib/ui/SettingsEditorContext.js +16 -0
  86. package/lib/ui/SettingsEditorLocaleContext.d.ts +13 -0
  87. package/lib/ui/SettingsEditorLocaleContext.js +15 -0
  88. package/lib/ui/SettingsEditorThemeContext.d.ts +9 -0
  89. package/lib/ui/SettingsEditorThemeContext.js +7 -0
  90. package/lib/ui/SettingsEditorThemePreferenceContext.d.ts +3 -0
  91. package/lib/ui/SettingsEditorThemePreferenceContext.js +7 -0
  92. package/lib/ui/SettingsEditorWrapper.d.ts +13 -0
  93. package/lib/ui/SettingsEditorWrapper.js +74 -0
  94. package/lib/ui/UserInterfaceCallbacksContext.d.ts +3 -0
  95. package/lib/ui/UserInterfaceCallbacksContext.js +7 -0
  96. package/lib/ui/UserInterfaceDataContext.d.ts +78 -0
  97. package/lib/ui/UserInterfaceDataContext.js +11 -0
  98. package/lib/ui/UserInterfaceLocaleContext.d.ts +13 -0
  99. package/lib/ui/UserInterfaceLocaleContext.js +15 -0
  100. package/lib/ui/UserInterfaceOwnTeamContext.d.ts +25 -0
  101. package/lib/ui/UserInterfaceOwnTeamContext.js +26 -0
  102. package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.d.ts +52 -0
  103. package/lib/ui/UserInterfaceOwnTeamJoinLinkContext.js +13 -0
  104. package/lib/ui/UserInterfaceParticipantContext.d.ts +16 -0
  105. package/lib/ui/UserInterfaceParticipantContext.js +24 -0
  106. package/lib/ui/UserInterfaceParticipantsContext.d.ts +12 -0
  107. package/lib/ui/UserInterfaceParticipantsContext.js +13 -0
  108. package/lib/ui/UserInterfaceRunKindContext.d.ts +31 -0
  109. package/lib/ui/UserInterfaceRunKindContext.js +31 -0
  110. package/lib/ui/UserInterfaceTeamsContext.d.ts +12 -0
  111. package/lib/ui/UserInterfaceTeamsContext.js +13 -0
  112. package/lib/ui/UserInterfaceThemeContext.d.ts +9 -0
  113. package/lib/ui/UserInterfaceThemeContext.js +7 -0
  114. package/lib/ui/UserInterfaceThemePreferenceContext.d.ts +3 -0
  115. package/lib/ui/UserInterfaceThemePreferenceContext.js +7 -0
  116. package/lib/ui/UserInterfaceTimerContext.d.ts +10 -0
  117. package/lib/ui/UserInterfaceTimerContext.js +12 -0
  118. package/lib/ui/UserInterfaceValidationContext.d.ts +25 -0
  119. package/lib/ui/UserInterfaceValidationContext.js +14 -0
  120. package/lib/ui/UserInterfaceViewerRoleContext.d.ts +14 -0
  121. package/lib/ui/UserInterfaceViewerRoleContext.js +17 -0
  122. package/lib/ui/UserInterfaceWrapper.d.ts +8 -0
  123. package/lib/ui/UserInterfaceWrapper.js +219 -0
  124. package/lib/ui/expectedParentOrigin.d.ts +10 -0
  125. package/lib/ui/expectedParentOrigin.js +32 -0
  126. package/lib/ui/index.d.ts +50 -0
  127. package/lib/ui/index.js +52 -0
  128. package/lib/ui/internal/SurfaceGate.d.ts +19 -0
  129. package/lib/ui/internal/SurfaceGate.js +17 -0
  130. package/lib/ui/internal/SurfaceScope.d.ts +17 -0
  131. package/lib/ui/internal/SurfaceScope.js +16 -0
  132. package/lib/ui/internal/createContexts.d.ts +50 -0
  133. package/lib/ui/internal/createContexts.js +32 -0
  134. package/lib/ui/internal/document-attributes.d.ts +9 -0
  135. package/lib/ui/internal/document-attributes.js +87 -0
  136. package/lib/ui/internal/surface-timer.d.ts +3 -0
  137. package/lib/ui/internal/surface-timer.js +11 -0
  138. package/lib/ui/internal/useLatestRef.d.ts +7 -0
  139. package/lib/ui/internal/useLatestRef.js +11 -0
  140. package/lib/ui/internal/useOptimisticMerge.d.ts +19 -0
  141. package/lib/ui/internal/useOptimisticMerge.js +28 -0
  142. package/lib/ui/internal/useSurfaceControls.d.ts +52 -0
  143. package/lib/ui/internal/useSurfaceControls.js +19 -0
  144. package/lib/ui/internal/useTypedPastState.d.ts +17 -0
  145. package/lib/ui/internal/useTypedPastState.js +17 -0
  146. package/lib/ui/mutation-commit.d.ts +32 -0
  147. package/lib/ui/mutation-commit.js +1 -0
  148. package/lib/ui/mutation-outcome.d.ts +66 -0
  149. package/lib/ui/mutation-outcome.js +49 -0
  150. package/lib/ui/request-id.d.ts +18 -0
  151. package/lib/ui/request-id.js +44 -0
  152. package/lib/ui/testing.d.ts +365 -0
  153. package/lib/ui/testing.js +572 -0
  154. package/lib/ui/tick-snapshot.d.ts +28 -0
  155. package/lib/ui/tick-snapshot.js +34 -0
  156. package/lib/ui/ui-protocol.d.ts +799 -0
  157. package/lib/ui/ui-protocol.js +50 -0
  158. package/lib/ui/useAsset.d.ts +80 -0
  159. package/lib/ui/useAsset.js +139 -0
  160. package/lib/ui/useEndpointCallbacks.d.ts +31 -0
  161. package/lib/ui/useEndpointCallbacks.js +68 -0
  162. package/lib/ui/useHostChannel.d.ts +90 -0
  163. package/lib/ui/useHostChannel.js +338 -0
  164. package/lib/ui/useMutationReporter.d.ts +100 -0
  165. package/lib/ui/useMutationReporter.js +97 -0
  166. package/lib/ui/usePastState.d.ts +39 -0
  167. package/lib/ui/usePastState.js +63 -0
  168. package/lib/ui/useViewerStorage.d.ts +54 -0
  169. package/lib/ui/useViewerStorage.js +141 -0
  170. package/package.json +83 -3
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Arguments for calling a logic function directly in a unit test.
3
+ *
4
+ * Every logic function takes one arguments object, and most of it is the
5
+ * runtime's to supply: `meta`, the roster, the callbacks, a session summary.
6
+ * A test cares about the context and perhaps one setting. Each builder here
7
+ * takes what the test cares about and fills the rest with fixed, deterministic
8
+ * values, typed so the result passes straight to the function with no cast.
9
+ *
10
+ * ```ts
11
+ * import { logicArgs } from '@seriousgaming/sdk/testing';
12
+ * import { defaultContext, defaultSettings } from './defaults';
13
+ * import { Run } from './logic';
14
+ *
15
+ * const result = await Run(
16
+ * logicArgs.run({ context: defaultContext, settings: defaultSettings, meta: { tick: 3 } })
17
+ * );
18
+ * ```
19
+ *
20
+ * Settings are not read from disk: there is no runtime here. Pass the
21
+ * resource's generated `defaultSettings`, spread with any value the test
22
+ * changes. A resource that declares no settings may leave `settings` out.
23
+ */
24
+ import { createStubCallbacks } from './stub-services.js';
25
+ /** The `meta.seed` every builder uses unless told otherwise. */
26
+ export const TEST_SEED = 'test-seed';
27
+ /** The `meta.now` every builder uses unless told otherwise: 2026-01-01T00:00:00Z. */
28
+ export const TEST_NOW = Date.UTC(2026, 0, 1);
29
+ function logicMeta(given) {
30
+ return { locale: 'en', seed: TEST_SEED, runKind: 'session', now: TEST_NOW, ...given };
31
+ }
32
+ function teamRunMeta(given, tick) {
33
+ const index = given?.index ?? 0;
34
+ return { ...logicMeta(given), tick, index, teamId: teamIdOf(index), ...given };
35
+ }
36
+ function teamIdOf(index) {
37
+ return `team-${index + 1}`;
38
+ }
39
+ /** One connected participant on `teamId`, with an id unique across the session. */
40
+ function participantOn(teamId, index) {
41
+ return { id: String(index + 1), name: 'Participant 1', index: 0, connected: true, teamId };
42
+ }
43
+ function settingsOf(input) {
44
+ // Absent only where SettingsInput let it be: a TSettings `{}` satisfies.
45
+ return (input.settings ?? {});
46
+ }
47
+ function callbacksOf(given) {
48
+ // The stubs are every callback there is, so they satisfy any subset the
49
+ // resource declares.
50
+ return { ...createStubCallbacks(), ...given };
51
+ }
52
+ function firstParticipant(participants, teamId) {
53
+ const participant = participants[0];
54
+ return participant
55
+ ? { role: 'participant', participant: { ...participant } }
56
+ : { role: 'observer', teamId };
57
+ }
58
+ function init(input) {
59
+ return engineArgs(input, 0);
60
+ }
61
+ function run(input) {
62
+ return engineArgs(input, 1);
63
+ }
64
+ function engineArgs(input, tick) {
65
+ const meta = teamRunMeta(input.meta, tick);
66
+ return {
67
+ context: input.context,
68
+ settings: settingsOf(input),
69
+ meta,
70
+ participants: input.participants ?? [participantOn(meta.teamId, meta.index)],
71
+ callbacks: callbacksOf(input.callbacks),
72
+ ...(input.state === undefined ? {} : { state: input.state }),
73
+ };
74
+ }
75
+ function aggregate(input) {
76
+ const teams = input.teams.map((team) => ({
77
+ ...team,
78
+ teamId: team.teamId ?? teamIdOf(team.index),
79
+ }));
80
+ const given = input;
81
+ return {
82
+ // Absent only where the type let it be: a TSharedContext `{}` satisfies.
83
+ shared: given.shared ?? { context: {} },
84
+ teams,
85
+ settings: settingsOf(input),
86
+ meta: { ...logicMeta(input.meta), tick: 1, ...input.meta },
87
+ participants: input.participants ?? teams.map((team) => participantOn(team.teamId, team.index)),
88
+ callbacks: callbacksOf(input.callbacks),
89
+ };
90
+ }
91
+ function hook(input) {
92
+ const { signal, meta, participants, callbacks, settings, ...payload } = input;
93
+ void signal;
94
+ void settings;
95
+ const subject = input.participant;
96
+ const args = {
97
+ ...payload,
98
+ settings: settingsOf(input),
99
+ meta: logicMeta(meta),
100
+ participants: participants ?? [subject],
101
+ callbacks: callbacksOf(callbacks),
102
+ };
103
+ // `payload` is the signal's payload and `context`, which is what the
104
+ // compiler cannot see through `HookPayloadMap[S]` with `S` unresolved.
105
+ return args;
106
+ }
107
+ function endpoint(input) {
108
+ const index = input.team?.index ?? 0;
109
+ const team = {
110
+ teamId: teamIdOf(index),
111
+ index,
112
+ name: `Team ${index + 1}`,
113
+ ...input.team,
114
+ };
115
+ const participants = input.participants ?? [participantOn(team.teamId, team.index)];
116
+ const role = input.caller?.role ?? 'participant';
117
+ const caller = {
118
+ ...(role === 'author' ? {} : { sessionId: 'test-session' }),
119
+ role,
120
+ ...(role === 'participant' || role === 'observer' ? { teamId: team.teamId } : {}),
121
+ ...(role === 'participant' && participants[0] ? { playerId: participants[0].id } : {}),
122
+ ...(role === 'external'
123
+ ? {}
124
+ : {
125
+ userInterfaceIdentifier: role === 'facilitator'
126
+ ? 'session'
127
+ : role === 'author'
128
+ ? 'scenario_editor'
129
+ : 'ui',
130
+ }),
131
+ ...input.caller,
132
+ };
133
+ const session = {
134
+ mode: 'independent',
135
+ completedTicks: 0,
136
+ maxTicks: null,
137
+ waitingRoomOpen: false,
138
+ complete: false,
139
+ teams: [{ ...team, completedTicks: 0, complete: false, timer: null }],
140
+ ...input.session,
141
+ };
142
+ const args = {
143
+ request: input.request,
144
+ context: input.context,
145
+ settings: settingsOf(input),
146
+ meta: logicMeta(input.meta),
147
+ participants,
148
+ callbacks: callbacksOf(input.callbacks),
149
+ caller,
150
+ session,
151
+ // Both halves: a team-scoped handler reads `team`, a session-scoped one
152
+ // `teams`, and the one a handler's type does not name is never read.
153
+ team,
154
+ ...(input.teams === undefined ? {} : { teams: input.teams }),
155
+ };
156
+ return args;
157
+ }
158
+ function tickController(input) {
159
+ const meta = teamRunMeta(input.meta, 1);
160
+ const teams = input.teams ?? [
161
+ { teamId: meta.teamId, index: meta.index, context: input.context },
162
+ ];
163
+ return {
164
+ context: input.context,
165
+ settings: settingsOf(input),
166
+ meta,
167
+ participants: input.participants ?? teams.map((team) => participantOn(team.teamId, team.index)),
168
+ callbacks: callbacksOf(input.callbacks),
169
+ teams,
170
+ waitingRoomOpen: input.waitingRoomOpen ?? false,
171
+ };
172
+ }
173
+ function validator(input) {
174
+ const participants = input.participants ?? [participantOn('team-1', 0)];
175
+ return {
176
+ previous: input.previous,
177
+ next: input.next,
178
+ actor: input.actor ?? firstParticipant(participants, 'team-1'),
179
+ settings: settingsOf(input),
180
+ meta: logicMeta(input.meta),
181
+ participants,
182
+ };
183
+ }
184
+ function requestTick(input) {
185
+ const participants = input.participants ?? [participantOn('team-1', 0)];
186
+ return {
187
+ context: input.context,
188
+ requestedTick: input.requestedTick ?? 1,
189
+ actor: input.actor ?? firstParticipant(participants, 'team-1'),
190
+ settings: settingsOf(input),
191
+ meta: logicMeta(input.meta),
192
+ participants,
193
+ };
194
+ }
195
+ /**
196
+ * Builders for each logic function's arguments. Each takes what the test
197
+ * names and fills the rest:
198
+ *
199
+ * - `meta`: `locale: 'en'`, `seed: 'test-seed'`, `runKind: 'session'`,
200
+ * `now` 2026-01-01T00:00:00Z, and where the function runs for a team,
201
+ * `index: 0` and `teamId: 'team-1'`. `tick` is 0 for `init` and 1 for the
202
+ * rest; `InitAggregate` passes `meta: { tick: 0 }`
203
+ * - `participants`: one connected participant on each team the call covers
204
+ * - `callbacks`: the offline stubs of `createStubCallbacks`, with any the
205
+ * test passes in their place
206
+ * - `settings`: required unless the resource declares none, since there is no
207
+ * runtime to resolve them. Pass the generated `defaultSettings`
208
+ *
209
+ * | Builder | For |
210
+ * | ---------------- | ----------------------------------------------- |
211
+ * | `init` | the engine's `Init` |
212
+ * | `run` | the engine's `Run`, and the steps it calls |
213
+ * | `aggregate` | `RunAggregate` and `InitAggregate` |
214
+ * | `hook` | a hook's `Run` |
215
+ * | `endpoint` | an endpoint's `Handle` or `Plan` |
216
+ * | `tickController` | `GetTickController` |
217
+ * | `validator` | a validator's `UpdateData` |
218
+ * | `requestTick` | a validator's `RequestTick` |
219
+ */
220
+ export const logicArgs = {
221
+ init,
222
+ run,
223
+ aggregate,
224
+ hook,
225
+ endpoint,
226
+ tickController,
227
+ validator,
228
+ requestTick,
229
+ };
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Deterministic, offline stand-ins for the callbacks logic receives.
3
+ *
4
+ * A test that reaches the platform fails without a network, without a key and
5
+ * in a sandbox, and passes or fails on what a model happened to say. These
6
+ * answer from the request alone, so the same request always gets the same
7
+ * answer and a test can assert on it.
8
+ *
9
+ * One implementation: `createTestSession` in `@seriousgaming/sdk-tools/client`
10
+ * answers its service callbacks with these too, storing the speech clip in the
11
+ * session's blob store rather than in memory.
12
+ */
13
+ import type { LLMGenerateOptions, LLMResponse, ExtractTextResponse, SignatureCallbacks } from '../build/index.js';
14
+ /** How long the stubbed speech clip is, in milliseconds. */
15
+ export declare const STUB_SPEECH_DURATION_MS = 100;
16
+ /**
17
+ * What a stubbed `generateText` answers for `request`: `[stub <hash>] <excerpt>`,
18
+ * an eight-digit hex hash of the whole request, then the first 60 characters
19
+ * of its prompt (the `prompt`, or the last `user` message's text) with
20
+ * whitespace collapsed, ending in `...` when it was cut. A request with a
21
+ * `jsonSchema` is answered with JSON that satisfies it.
22
+ */
23
+ export declare function stubGenerateText(request: LLMGenerateOptions | string): LLMResponse;
24
+ /** What a stubbed `extractText` answers: `[stub] text of <reference>`, without reading the file. */
25
+ export declare function stubExtractText(reference: string): ExtractTextResponse;
26
+ /** The clip a stubbed `generateSpeech` stores: 100ms of 8-bit mono silence, as a WAV file. */
27
+ export declare function stubSpeechAudio(): Uint8Array;
28
+ /**
29
+ * Every callback logic can receive, stubbed, for calling a logic function
30
+ * directly in a unit test.
31
+ *
32
+ * - `generateText`, `extractText`: {@link stubGenerateText}, {@link stubExtractText}
33
+ * - `generateSpeech`: stores {@link stubSpeechAudio} and returns its `blob:` reference
34
+ * - `writeBlob`: stores the bytes and returns `blob:stub-<n>`, numbered from 1
35
+ * - `readAsset`: reads back a `blob:` reference written here, and rejects
36
+ * anything else, naming it, since there is no scenario to read `asset:` from
37
+ * - `getPastState`: answers `[]`, as a session's first round does
38
+ *
39
+ * Blobs live in memory, one store per call, so two tests never share one.
40
+ */
41
+ export declare function createStubCallbacks(): Required<SignatureCallbacks>;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Deterministic, offline stand-ins for the callbacks logic receives.
3
+ *
4
+ * A test that reaches the platform fails without a network, without a key and
5
+ * in a sandbox, and passes or fails on what a model happened to say. These
6
+ * answer from the request alone, so the same request always gets the same
7
+ * answer and a test can assert on it.
8
+ *
9
+ * One implementation: `createTestSession` in `@seriousgaming/sdk-tools/client`
10
+ * answers its service callbacks with these too, storing the speech clip in the
11
+ * session's blob store rather than in memory.
12
+ */
13
+ /** The longest prompt excerpt a stubbed `generateText` repeats back. */
14
+ const EXCERPT_LENGTH = 60;
15
+ /** How long the stubbed speech clip is, in milliseconds. */
16
+ export const STUB_SPEECH_DURATION_MS = 100;
17
+ const SAMPLE_RATE = 8000;
18
+ /**
19
+ * What a stubbed `generateText` answers for `request`: `[stub <hash>] <excerpt>`,
20
+ * an eight-digit hex hash of the whole request, then the first 60 characters
21
+ * of its prompt (the `prompt`, or the last `user` message's text) with
22
+ * whitespace collapsed, ending in `...` when it was cut. A request with a
23
+ * `jsonSchema` is answered with JSON that satisfies it.
24
+ */
25
+ export function stubGenerateText(request) {
26
+ const options = typeof request === 'string' ? { prompt: request } : request;
27
+ const text = options.jsonSchema === undefined
28
+ ? `[stub ${fnv1a(JSON.stringify(options))}] ${excerpt(promptText(options))}`
29
+ : JSON.stringify(valueFor(options.jsonSchema));
30
+ return {
31
+ content: [{ type: 'text', text }],
32
+ stopReason: 'endTurn',
33
+ usage: { inputTokens: 0, outputTokens: 0 },
34
+ };
35
+ }
36
+ /** What a stubbed `extractText` answers: `[stub] text of <reference>`, without reading the file. */
37
+ export function stubExtractText(reference) {
38
+ return { content: `[stub] text of ${reference}`, mimeType: 'text/plain' };
39
+ }
40
+ /** The clip a stubbed `generateSpeech` stores: 100ms of 8-bit mono silence, as a WAV file. */
41
+ export function stubSpeechAudio() {
42
+ const samples = (SAMPLE_RATE * STUB_SPEECH_DURATION_MS) / 1000;
43
+ const bytes = new Uint8Array(44 + samples);
44
+ const view = new DataView(bytes.buffer);
45
+ const ascii = (text, at) => {
46
+ for (let i = 0; i < text.length; i++)
47
+ bytes[at + i] = text.charCodeAt(i);
48
+ };
49
+ ascii('RIFF', 0);
50
+ view.setUint32(4, 36 + samples, true);
51
+ ascii('WAVE', 8);
52
+ ascii('fmt ', 12);
53
+ view.setUint32(16, 16, true);
54
+ view.setUint16(20, 1, true);
55
+ view.setUint16(22, 1, true);
56
+ view.setUint32(24, SAMPLE_RATE, true);
57
+ view.setUint32(28, SAMPLE_RATE, true);
58
+ view.setUint16(32, 1, true);
59
+ view.setUint16(34, 8, true);
60
+ ascii('data', 36);
61
+ view.setUint32(40, samples, true);
62
+ // 8-bit PCM is unsigned, so silence is the midpoint rather than zero.
63
+ bytes.fill(0x80, 44);
64
+ return bytes;
65
+ }
66
+ /**
67
+ * Every callback logic can receive, stubbed, for calling a logic function
68
+ * directly in a unit test.
69
+ *
70
+ * - `generateText`, `extractText`: {@link stubGenerateText}, {@link stubExtractText}
71
+ * - `generateSpeech`: stores {@link stubSpeechAudio} and returns its `blob:` reference
72
+ * - `writeBlob`: stores the bytes and returns `blob:stub-<n>`, numbered from 1
73
+ * - `readAsset`: reads back a `blob:` reference written here, and rejects
74
+ * anything else, naming it, since there is no scenario to read `asset:` from
75
+ * - `getPastState`: answers `[]`, as a session's first round does
76
+ *
77
+ * Blobs live in memory, one store per call, so two tests never share one.
78
+ */
79
+ export function createStubCallbacks() {
80
+ const blobs = new Map();
81
+ const store = (data, name, mimeType) => {
82
+ const reference = `blob:stub-${blobs.size + 1}`;
83
+ blobs.set(reference, { data, name, mimeType });
84
+ return reference;
85
+ };
86
+ return {
87
+ generateText: async (request) => stubGenerateText(request),
88
+ extractText: async (reference) => stubExtractText(reference),
89
+ generateSpeech: async () => {
90
+ const response = {
91
+ reference: store(stubSpeechAudio(), 'speech.wav', 'audio/wav'),
92
+ mimeType: 'audio/wav',
93
+ durationMs: STUB_SPEECH_DURATION_MS,
94
+ charactersBilled: 0,
95
+ };
96
+ return response;
97
+ },
98
+ writeBlob: async (data, options) => store(data instanceof Uint8Array ? data : new Uint8Array(data), options.name, options.mimeType),
99
+ readAsset: async (reference) => {
100
+ const blob = blobs.get(reference);
101
+ if (!blob) {
102
+ throw new Error(`No stubbed asset "${reference}": only a blob written in this test can be read. ` +
103
+ 'Pass your own readAsset in callbacks to serve anything else');
104
+ }
105
+ return blob;
106
+ },
107
+ getPastState: async () => [],
108
+ };
109
+ }
110
+ function promptText(options) {
111
+ if (options.prompt !== undefined)
112
+ return options.prompt;
113
+ const last = [...(options.messages ?? [])].reverse().find((m) => m.role === 'user');
114
+ if (!last)
115
+ return '';
116
+ if (typeof last.content === 'string')
117
+ return last.content;
118
+ return last.content.map((block) => (block.type === 'text' ? block.text : '')).join(' ');
119
+ }
120
+ function excerpt(text) {
121
+ const collapsed = text.replace(/\s+/g, ' ').trim();
122
+ return collapsed.length > EXCERPT_LENGTH
123
+ ? `${collapsed.slice(0, EXCERPT_LENGTH).trimEnd()}...`
124
+ : collapsed;
125
+ }
126
+ /** 32-bit FNV-1a, as eight hex digits: short, stable and dependency-free. */
127
+ function fnv1a(text) {
128
+ let hash = 0x811c9dc5;
129
+ for (let i = 0; i < text.length; i++) {
130
+ hash ^= text.charCodeAt(i);
131
+ hash = Math.imul(hash, 0x01000193) >>> 0;
132
+ }
133
+ return hash.toString(16).padStart(8, '0');
134
+ }
135
+ /**
136
+ * The smallest value that satisfies `schema`: its `const`, its first `enum`
137
+ * member or its `default` where it has one, every required property of an
138
+ * object (every property, when none is required), and the minimum length of
139
+ * an array.
140
+ */
141
+ function valueFor(schema) {
142
+ const loose = schema;
143
+ if (loose.const !== undefined)
144
+ return loose.const;
145
+ if (Array.isArray(schema.enum) && schema.enum.length > 0)
146
+ return schema.enum[0];
147
+ if (loose.default !== undefined)
148
+ return loose.default;
149
+ const alternative = loose.anyOf?.[0] ?? loose.oneOf?.[0];
150
+ if (alternative)
151
+ return valueFor(alternative);
152
+ const type = Array.isArray(schema.type) ? schema.type[0] : schema.type;
153
+ switch (type) {
154
+ case 'object': {
155
+ const properties = schema.properties ?? {};
156
+ const keys = schema.required ?? Object.keys(properties);
157
+ return Object.fromEntries(keys.map((key) => [key, properties[key] ? valueFor(properties[key]) : null]));
158
+ }
159
+ case 'array': {
160
+ const count = loose.minItems ?? 0;
161
+ const item = schema.items;
162
+ return Array.from({ length: count }, () => (item ? valueFor(item) : null));
163
+ }
164
+ case 'string':
165
+ return '[stub]';
166
+ case 'number':
167
+ case 'integer':
168
+ return loose.minimum ?? 0;
169
+ case 'boolean':
170
+ return false;
171
+ case 'null':
172
+ return null;
173
+ default:
174
+ return schema.properties ? valueFor({ ...schema, type: 'object' }) : null;
175
+ }
176
+ }
@@ -0,0 +1,36 @@
1
+ import type { TeamStateEntry } from '../build/callbacks.js';
2
+ export type { TeamStateEntry };
3
+ /**
4
+ * Every team's live cross-team state, one entry per team, sorted by stable
5
+ * team index (§5.6) and including the calling team's own.
6
+ *
7
+ * Keyed by team rather than by capability, matching what `getPastState`
8
+ * returns: `entry.context.<capability>` reads the same whether the entry came
9
+ * from this hook or from a historical read, so one cell renderer serves both.
10
+ */
11
+ export type CrossTeamData = TeamStateEntry[];
12
+ declare const CrossTeamDataContext: import("react").Context<CrossTeamData | undefined>;
13
+ /**
14
+ * Observe every team's live cross-team state, including the calling team's
15
+ * own. Entries update as teams change their pending state, without waiting for
16
+ * a tick.
17
+ *
18
+ * Each entry's `context` carries exactly the capabilities this user interface
19
+ * declared with `crossTeam: true`, so there is no capability identifier to
20
+ * pass and none to misspell: pass the generated cross-team context as the type
21
+ * argument and read fields off it.
22
+ *
23
+ * ```typescript
24
+ * const teams = useUserInterfaceCrossTeamData<Context>();
25
+ * const total = teams.reduce((sum, team) => sum + team.context.decisions.Price, 0);
26
+ * ```
27
+ *
28
+ * Requires each capability to declare `scope: 'team-readable'` and this
29
+ * interface to declare the read with `crossTeam: true`. Returns an empty array
30
+ * until the host delivers the first values.
31
+ *
32
+ * The historical counterpart is `getPastState({ teams: 'all' })`, whose entries
33
+ * are these plus the tick they were captured at.
34
+ */
35
+ declare function useUserInterfaceCrossTeamData<T = Record<string, unknown>>(): TeamStateEntry<T>[];
36
+ export { useUserInterfaceCrossTeamData, CrossTeamDataContext };
@@ -0,0 +1,32 @@
1
+ import { createContext, useContext } from 'react';
2
+ const CrossTeamDataContext = createContext(undefined);
3
+ /**
4
+ * Observe every team's live cross-team state, including the calling team's
5
+ * own. Entries update as teams change their pending state, without waiting for
6
+ * a tick.
7
+ *
8
+ * Each entry's `context` carries exactly the capabilities this user interface
9
+ * declared with `crossTeam: true`, so there is no capability identifier to
10
+ * pass and none to misspell: pass the generated cross-team context as the type
11
+ * argument and read fields off it.
12
+ *
13
+ * ```typescript
14
+ * const teams = useUserInterfaceCrossTeamData<Context>();
15
+ * const total = teams.reduce((sum, team) => sum + team.context.decisions.Price, 0);
16
+ * ```
17
+ *
18
+ * Requires each capability to declare `scope: 'team-readable'` and this
19
+ * interface to declare the read with `crossTeam: true`. Returns an empty array
20
+ * until the host delivers the first values.
21
+ *
22
+ * The historical counterpart is `getPastState({ teams: 'all' })`, whose entries
23
+ * are these plus the tick they were captured at.
24
+ */
25
+ function useUserInterfaceCrossTeamData() {
26
+ const ctx = useContext(CrossTeamDataContext);
27
+ if (!ctx) {
28
+ throw new Error('useUserInterfaceCrossTeamData must be used inside a UserInterfaceWrapper');
29
+ }
30
+ return ctx;
31
+ }
32
+ export { useUserInterfaceCrossTeamData, CrossTeamDataContext };
@@ -0,0 +1,72 @@
1
+ import type { PastStateEntry, PastStateQuery } from '../build/callbacks.js';
2
+ import type { SessionMode } from '../build/signatures.js';
3
+ import type { SessionData } from './SessionUserInterfaceDataContext.js';
4
+ import type { TeamInfo } from './SessionUserInterfaceTeamsContext.js';
5
+ import type { SessionUserInterfaceTheme } from './SessionUserInterfaceThemeContext.js';
6
+ import type { UserInterfaceLocale } from './UserInterfaceLocaleContext.js';
7
+ /**
8
+ * The game a presentation belongs to, as the active scenario resolves it
9
+ * (§6.15). Known with or without a session.
10
+ */
11
+ export interface PresentationGame {
12
+ /** The game's name in the current locale. */
13
+ name: string;
14
+ /**
15
+ * How many rounds it runs for (the `game` settings' `maxTicks`), or `null`
16
+ * when its engine or tick controller decides.
17
+ */
18
+ maxTicks: number | null;
19
+ mode: SessionMode;
20
+ /**
21
+ * The active scenario's display name, or `null` with none. For showing,
22
+ * never for branching on: a facilitator's own scenario saved from it has a
23
+ * different identity. Vary a deck on settings and scenario assets instead.
24
+ */
25
+ scenarioName: string | null;
26
+ }
27
+ /**
28
+ * The session a presentation is shown during (§6.15): the shared store and
29
+ * every team, narrowed to what the presentation `reads`.
30
+ */
31
+ export interface PresentationSession<TTeamContext = Record<string, unknown>, TSharedContext = Record<string, unknown>> {
32
+ /**
33
+ * Stable for the life of the session and unique to it, so a deck can key
34
+ * anything it remembers by session, such as the slide it was on.
35
+ */
36
+ id: string;
37
+ /** The session's name, as the facilitator sees it. */
38
+ name: string;
39
+ /** The most ticks any team has completed; 0 before the first. */
40
+ completedTicks: number;
41
+ /** Shared state and each team's state, keyed by team id. */
42
+ data: SessionData<TTeamContext, TSharedContext>;
43
+ /** Every team, sorted by index. */
44
+ teams: TeamInfo[];
45
+ }
46
+ export interface PresentationValue<TSettings, TTeamContext, TSharedContext> {
47
+ game: PresentationGame;
48
+ /**
49
+ * The session it is shown during, or `null` when it is shown with none: in
50
+ * a game's library, while rehearsing, or to export it beforehand. A deck
51
+ * MUST render either way, showing sample or placeholder data in place of
52
+ * results it does not have.
53
+ */
54
+ session: PresentationSession<TTeamContext, TSharedContext> | null;
55
+ /** The settings resources the presentation declares, resolved for the scenario. */
56
+ settings: TSettings;
57
+ theme: SessionUserInterfaceTheme;
58
+ locale: UserInterfaceLocale;
59
+ /**
60
+ * Read past rounds (§12.5), as the session UI does. Rejects when there is
61
+ * no session.
62
+ */
63
+ getPastState: <T = Record<string, unknown>>(query: PastStateQuery<T>) => Promise<PastStateEntry<T>[]>;
64
+ /** Resolve an `asset:`, `shared:` or `blob:` reference to a URL. */
65
+ getAssetUrl: (reference: string) => string;
66
+ }
67
+ export declare const PresentationContext: import("react").Context<PresentationValue<unknown, unknown, unknown> | undefined>;
68
+ /**
69
+ * Everything a presentation can see: the session (or `null`), its settings,
70
+ * the host's theme, and the locale.
71
+ */
72
+ export declare function usePresentation<TSettings = Record<string, Record<string, unknown>>, TTeamContext = Record<string, unknown>, TSharedContext = Record<string, unknown>>(): PresentationValue<TSettings, TTeamContext, TSharedContext>;
@@ -0,0 +1,13 @@
1
+ import { createRequiredContext } from './internal/createContexts.js';
2
+ const { Context, useValue } = createRequiredContext({
3
+ hookName: 'usePresentation',
4
+ wrapperName: 'PresentationWrapper',
5
+ });
6
+ export const PresentationContext = Context;
7
+ /**
8
+ * Everything a presentation can see: the session (or `null`), its settings,
9
+ * the host's theme, and the locale.
10
+ */
11
+ export function usePresentation() {
12
+ return useValue();
13
+ }
@@ -0,0 +1,12 @@
1
+ import { ReactNode } from 'react';
2
+ interface PresentationWrapperProps {
3
+ children?: ReactNode;
4
+ fallback?: ReactNode;
5
+ targetWindow?: Window;
6
+ }
7
+ /**
8
+ * Wrap a presentation bundle (§6.15). Read-only: there is nothing here to
9
+ * write with, and the host would refuse it if there were.
10
+ */
11
+ export declare function PresentationWrapper({ children, fallback, targetWindow, }: PresentationWrapperProps): import("react").JSX.Element;
12
+ export {};