@superbuilders/primer-tives 3.5.0 → 3.5.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.
- package/README.md +1098 -156
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +2 -2
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ The public lifecycle is one async call:
|
|
|
8
8
|
create(options) -> Promise<PrimerState>
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
`create` resolves learner auth, opens the first Primer
|
|
11
|
+
`create` resolves learner auth, opens the first Primer learning state, and returns the live state machine object your renderer drives.
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
14
|
bun add @superbuilders/primer-tives
|
|
@@ -16,33 +16,31 @@ bun add @superbuilders/primer-tives
|
|
|
16
16
|
|
|
17
17
|
## Version
|
|
18
18
|
|
|
19
|
-
The current SDK version is `3.5.
|
|
20
|
-
|
|
21
|
-
The SDK sends `X-Primer-SDK-Version: 3.5.0` on `/api/v0/advance`.
|
|
19
|
+
The current SDK version is `3.5.1`.
|
|
22
20
|
|
|
23
21
|
## Entrypoints
|
|
24
22
|
|
|
25
|
-
There is no package-root export.
|
|
23
|
+
There is no package-root export. Import from the public subpath that owns the surface you need.
|
|
26
24
|
|
|
27
25
|
| Subpath | Owns |
|
|
28
26
|
| --- | --- |
|
|
29
|
-
| `@superbuilders/primer-tives/client` | `create`, `PrimerOptions`, `PrimerState`, state interfaces, PCI render props |
|
|
30
|
-
| `@superbuilders/primer-tives/contracts` | Content, stimulus, interaction, submission, review, PCI types, validation helpers |
|
|
31
|
-
| `@superbuilders/primer-tives/errors` | SDK error
|
|
27
|
+
| `@superbuilders/primer-tives/client` | `create`, `PrimerOptions`, `PrimerState`, all state interfaces, PCI render props |
|
|
28
|
+
| `@superbuilders/primer-tives/contracts` | Content, stimulus, interaction, submission, review, PCI types, schemas, validation helpers |
|
|
29
|
+
| `@superbuilders/primer-tives/errors` | Every SDK error sentinel |
|
|
32
30
|
| `@superbuilders/primer-tives/logger` | `PrimerLogger` interface |
|
|
33
31
|
| `@superbuilders/primer-tives/subject` | `Subject`, `SUBJECTS` |
|
|
34
|
-
| `@superbuilders/primer-tives/subject-pcis` | Subject-required PCI helpers |
|
|
32
|
+
| `@superbuilders/primer-tives/subject-pcis` | Subject-required PCI helpers and type helpers |
|
|
35
33
|
| `@superbuilders/primer-tives/grade-level` | `GradeLevel`, `GRADE_LEVELS` |
|
|
36
34
|
|
|
37
35
|
## Quick Start
|
|
38
36
|
|
|
39
|
-
Math
|
|
37
|
+
Math content can require the fraction-input PCI capability, so a math renderer must declare it.
|
|
40
38
|
|
|
41
39
|
```ts
|
|
42
40
|
import * as logger from "@superbuilders/slog"
|
|
43
41
|
import { create } from "@superbuilders/primer-tives/client"
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
let state = await create({
|
|
46
44
|
origin: "https://primerlearn.dev",
|
|
47
45
|
publishableKey: "pk_...",
|
|
48
46
|
subject: "math",
|
|
@@ -51,10 +49,10 @@ const state = await create({
|
|
|
51
49
|
})
|
|
52
50
|
```
|
|
53
51
|
|
|
54
|
-
If
|
|
52
|
+
If your application already has a learner access token, pass it directly. The SDK will use that token and skip SDK-managed auth.
|
|
55
53
|
|
|
56
54
|
```ts
|
|
57
|
-
|
|
55
|
+
let state = await create({
|
|
58
56
|
origin: "https://primerlearn.dev",
|
|
59
57
|
publishableKey: "pk_...",
|
|
60
58
|
accessToken,
|
|
@@ -64,10 +62,10 @@ const state = await create({
|
|
|
64
62
|
})
|
|
65
63
|
```
|
|
66
64
|
|
|
67
|
-
Vocabulary
|
|
65
|
+
Vocabulary and science currently have no required PCI capabilities, so `supportedPcis` can be omitted for those subjects.
|
|
68
66
|
|
|
69
67
|
```ts
|
|
70
|
-
|
|
68
|
+
let state = await create({
|
|
71
69
|
origin: "https://primerlearn.dev",
|
|
72
70
|
publishableKey: "pk_...",
|
|
73
71
|
subject: "vocabulary",
|
|
@@ -75,10 +73,10 @@ const state = await create({
|
|
|
75
73
|
})
|
|
76
74
|
```
|
|
77
75
|
|
|
78
|
-
Omitting `subject` means the SDK asks Primer for the
|
|
76
|
+
Omitting `subject` means the SDK asks Primer for the all-subject runtime scope. Because that scope can include math, it requires the union of all subject-required PCIs.
|
|
79
77
|
|
|
80
78
|
```ts
|
|
81
|
-
|
|
79
|
+
let state = await create({
|
|
82
80
|
origin: "https://primerlearn.dev",
|
|
83
81
|
publishableKey: "pk_...",
|
|
84
82
|
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
@@ -86,10 +84,60 @@ const state = await create({
|
|
|
86
84
|
})
|
|
87
85
|
```
|
|
88
86
|
|
|
89
|
-
##
|
|
87
|
+
## `create(options)`
|
|
90
88
|
|
|
91
89
|
```ts
|
|
92
|
-
|
|
90
|
+
function create<
|
|
91
|
+
const S extends Subject | undefined = undefined,
|
|
92
|
+
const Supported extends readonly PciId[] = []
|
|
93
|
+
>(options: PrimerOptions<S, Supported>): Promise<PrimerState>
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`create` can fail in two different ways:
|
|
97
|
+
|
|
98
|
+
| Moment | Behavior |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| Auth or local token resolution fails | `create` rejects with an SDK sentinel error. |
|
|
101
|
+
| The learning runtime cannot produce a normal first state | `create` resolves to `ErroredState` or `FatalState`. |
|
|
102
|
+
|
|
103
|
+
This distinction matters. Use `errors.try(create(...))` for startup failures, then handle `state.phase` for learning-state failures.
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
import * as errors from "@superbuilders/errors"
|
|
107
|
+
import * as logger from "@superbuilders/slog"
|
|
108
|
+
import { create } from "@superbuilders/primer-tives/client"
|
|
109
|
+
import { ErrAuthPopupBlocked, ErrMalformedAccessToken } from "@superbuilders/primer-tives/errors"
|
|
110
|
+
|
|
111
|
+
const result = await errors.try(
|
|
112
|
+
create({
|
|
113
|
+
origin,
|
|
114
|
+
publishableKey,
|
|
115
|
+
accessToken,
|
|
116
|
+
subject: "math",
|
|
117
|
+
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
118
|
+
logger
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
if (result.error) {
|
|
122
|
+
if (errors.is(result.error, ErrAuthPopupBlocked)) {
|
|
123
|
+
renderStartButtonAgain()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
if (errors.is(result.error, ErrMalformedAccessToken)) {
|
|
127
|
+
renderSignInAgain()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
logger.error("primer create failed", { error: result.error })
|
|
131
|
+
throw result.error
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let state = result.data
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## `PrimerOptions`
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
type PrimerOptions<S extends Subject | undefined = undefined, Supported extends readonly PciId[] = []> = {
|
|
93
141
|
readonly origin: string
|
|
94
142
|
readonly publishableKey: string
|
|
95
143
|
readonly accessToken?: string
|
|
@@ -101,50 +149,102 @@ type PrimerOptions<S extends Subject | undefined = undefined, Pcis extends PciId
|
|
|
101
149
|
}
|
|
102
150
|
```
|
|
103
151
|
|
|
104
|
-
| Field | Meaning |
|
|
152
|
+
| Field | Required | Meaning |
|
|
153
|
+
| --- | --- | --- |
|
|
154
|
+
| `origin` | Yes | Primer deployment origin. |
|
|
155
|
+
| `publishableKey` | Yes | Public key identifying the Primer frontend your runtime belongs to. |
|
|
156
|
+
| `accessToken` | No | Learner access token. When present, SDK-managed auth is skipped. |
|
|
157
|
+
| `subject` | No | Public content scope: `"math"`, `"vocabulary"`, or `"science"`. Omitted means all-subject scope. |
|
|
158
|
+
| `supportedPcis` | Subject-dependent | Renderer capabilities for Portable Custom Interactions. Required when the chosen scope can emit required PCIs. |
|
|
159
|
+
| `fetch` | No | Fetch override for tests, instrumentation, or host runtime integration. |
|
|
160
|
+
| `abort` | No | Abort controller for SDK runtime work. |
|
|
161
|
+
| `logger` | Yes | Structured logger implementing `debug`, `info`, `warn`, and `error`. |
|
|
162
|
+
|
|
163
|
+
The presence or absence of `accessToken` selects auth semantics.
|
|
164
|
+
|
|
165
|
+
| Shape | Semantics |
|
|
105
166
|
| --- | --- |
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
|
167
|
+
| `accessToken` present | The SDK validates the token shape locally and uses it for learning runtime state. |
|
|
168
|
+
| `accessToken` absent | The SDK resolves a learner token through SDK-managed browser auth, then starts learning runtime state. |
|
|
169
|
+
|
|
170
|
+
The public API does not expose separate auth plumbing, auth result objects, or any auth lifecycle method. Auth is part of `create`.
|
|
171
|
+
|
|
172
|
+
## Auth Semantics
|
|
173
|
+
|
|
174
|
+
An access token is expected to be JWS-shaped: it starts with `eyJ` and contains exactly two dots. If a provided or SDK-managed token does not match that shape, `create` rejects with `ErrMalformedAccessToken`.
|
|
175
|
+
|
|
176
|
+
SDK-managed auth may require browser capabilities and learner interaction. These failures reject `create`:
|
|
177
|
+
|
|
178
|
+
| Sentinel | Meaning |
|
|
118
179
|
| --- | --- |
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
180
|
+
| `ErrAuthUnavailable` | SDK-managed auth requires browser functionality that is unavailable in the current runtime. |
|
|
181
|
+
| `ErrAuthConfigInvalid` | SDK-managed auth was given invalid public configuration. |
|
|
182
|
+
| `ErrAuthCallbackInvalid` | The auth result could not be accepted as a successful learner auth result. |
|
|
183
|
+
| `ErrAuthStateMismatch` | The auth result did not match the auth attempt that initiated it. |
|
|
184
|
+
| `ErrAuthPopupBlocked` | The browser blocked the learner auth window. |
|
|
185
|
+
| `ErrAuthCancelled` | The learner auth interaction was closed or exceeded its allowed time. |
|
|
186
|
+
| `ErrMalformedAccessToken` | The resolved token was not shaped like a learner access token. |
|
|
121
187
|
|
|
122
|
-
|
|
188
|
+
Applications should handle user-actionable auth failures directly and log unexpected failures before propagating them.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import * as errors from "@superbuilders/errors"
|
|
192
|
+
import * as logger from "@superbuilders/slog"
|
|
193
|
+
import {
|
|
194
|
+
ErrAuthCancelled,
|
|
195
|
+
ErrAuthPopupBlocked,
|
|
196
|
+
ErrAuthUnavailable
|
|
197
|
+
} from "@superbuilders/primer-tives/errors"
|
|
198
|
+
|
|
199
|
+
const result = await errors.try(create(options))
|
|
200
|
+
if (result.error) {
|
|
201
|
+
if (errors.is(result.error, ErrAuthPopupBlocked)) {
|
|
202
|
+
renderPopupInstructions()
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
if (errors.is(result.error, ErrAuthCancelled)) {
|
|
206
|
+
renderTryAgain()
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
if (errors.is(result.error, ErrAuthUnavailable)) {
|
|
210
|
+
renderUnsupportedBrowserMessage()
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
logger.error("primer auth failed", { error: result.error })
|
|
214
|
+
throw result.error
|
|
215
|
+
}
|
|
216
|
+
```
|
|
123
217
|
|
|
124
218
|
## Subject And PCI Contract
|
|
125
219
|
|
|
126
220
|
`subject` selects content scope and determines required renderer capabilities.
|
|
127
221
|
|
|
128
|
-
|
|
222
|
+
```ts
|
|
223
|
+
import { SUBJECTS } from "@superbuilders/primer-tives/subject"
|
|
224
|
+
import type { Subject } from "@superbuilders/primer-tives/subject"
|
|
129
225
|
|
|
130
|
-
|
|
226
|
+
const subjects = SUBJECTS
|
|
227
|
+
type RuntimeSubject = Subject
|
|
228
|
+
```
|
|
131
229
|
|
|
132
|
-
|
|
230
|
+
Current public subjects:
|
|
231
|
+
|
|
232
|
+
| Subject | Required PCI support |
|
|
133
233
|
| --- | --- |
|
|
134
|
-
| omitted | union of all subject-required PCIs, currently `"urn:primer:pci:fraction-input"` |
|
|
135
234
|
| `"math"` | `"urn:primer:pci:fraction-input"` |
|
|
136
235
|
| `"vocabulary"` | none |
|
|
137
236
|
| `"science"` | none |
|
|
237
|
+
| omitted subject | union of all subject-required PCIs, currently `"urn:primer:pci:fraction-input"` |
|
|
138
238
|
|
|
139
|
-
The
|
|
239
|
+
The type-level rule is a subset check:
|
|
140
240
|
|
|
141
241
|
```txt
|
|
142
|
-
required PCIs for
|
|
242
|
+
required PCIs for selected scope <= supportedPcis
|
|
143
243
|
```
|
|
144
244
|
|
|
145
|
-
Order does not matter. Extra supported PCIs are allowed.
|
|
245
|
+
Order does not matter. Extra supported PCIs are allowed. `supportedPcis` is a renderer capability declaration, not a content request.
|
|
146
246
|
|
|
147
|
-
This fails at compile time:
|
|
247
|
+
This fails at compile time because math can emit a required PCI:
|
|
148
248
|
|
|
149
249
|
```ts
|
|
150
250
|
await create({
|
|
@@ -167,13 +267,81 @@ await create({
|
|
|
167
267
|
})
|
|
168
268
|
```
|
|
169
269
|
|
|
170
|
-
If `subject` is a broad dynamic `Subject` value, TypeScript requires the union of
|
|
270
|
+
If `subject` is a broad dynamic `Subject` value, TypeScript requires support for the union of all PCIs required by the possible subjects.
|
|
171
271
|
|
|
172
|
-
|
|
272
|
+
```ts
|
|
273
|
+
declare const subject: Subject
|
|
173
274
|
|
|
174
|
-
|
|
275
|
+
await create({
|
|
276
|
+
origin,
|
|
277
|
+
publishableKey,
|
|
278
|
+
subject,
|
|
279
|
+
supportedPcis: ["urn:primer:pci:fraction-input"],
|
|
280
|
+
logger
|
|
281
|
+
})
|
|
282
|
+
```
|
|
175
283
|
|
|
176
|
-
|
|
284
|
+
The runtime also protects the trust boundary. If Primer presents a portable custom interaction that is not declared in `supportedPcis`, the SDK returns `FatalState` with `ErrUnsupportedPci`.
|
|
285
|
+
|
|
286
|
+
## Runtime Loop
|
|
287
|
+
|
|
288
|
+
Every renderer should switch on `state.phase`. Interaction rendering should then switch on `state.kind`.
|
|
289
|
+
|
|
290
|
+
```ts
|
|
291
|
+
import * as logger from "@superbuilders/slog"
|
|
292
|
+
import type { PrimerState } from "@superbuilders/primer-tives/client"
|
|
293
|
+
|
|
294
|
+
async function runPrimer(initialState: PrimerState): Promise<void> {
|
|
295
|
+
let state = initialState
|
|
296
|
+
|
|
297
|
+
while (state.phase !== "completed" && state.phase !== "fatal") {
|
|
298
|
+
switch (state.phase) {
|
|
299
|
+
case "observation":
|
|
300
|
+
renderFrame(state.body, state.stimulus)
|
|
301
|
+
state = await state.advance()
|
|
302
|
+
break
|
|
303
|
+
case "interaction":
|
|
304
|
+
state = await renderAndSubmitInteraction(state)
|
|
305
|
+
break
|
|
306
|
+
case "feedback":
|
|
307
|
+
renderFeedback(state.feedbackContent, state.isCorrect, state.review)
|
|
308
|
+
state = await state.advance()
|
|
309
|
+
break
|
|
310
|
+
case "errored":
|
|
311
|
+
if (state.retriable) {
|
|
312
|
+
state = await state.retry()
|
|
313
|
+
break
|
|
314
|
+
}
|
|
315
|
+
logger.error("primer state error", { error: state.error })
|
|
316
|
+
throw state.error
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (state.phase === "fatal") {
|
|
321
|
+
logger.error("primer fatal state", { error: state.error })
|
|
322
|
+
throw state.error
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
State transitions:
|
|
328
|
+
|
|
329
|
+
| Current state | Valid operation | Next result |
|
|
330
|
+
| --- | --- | --- |
|
|
331
|
+
| `ObservationState` | `advance()` | `Promise<PrimerState>` |
|
|
332
|
+
| `ChoiceState` | `submitChoice(selectedKeys)` or `timeout()` | `Promise<PrimerState>` |
|
|
333
|
+
| `TextEntryState` | `submitText(value)` or `timeout()` | `Promise<PrimerState>` |
|
|
334
|
+
| `ExtendedTextSingleState` | `submitText(value)` or `timeout()` | `Promise<PrimerState>` |
|
|
335
|
+
| `ExtendedTextMultipleState` | `submitTexts(values)` or `timeout()` | `Promise<PrimerState>` |
|
|
336
|
+
| `OrderState` | `submitOrder(orderedKeys)` or `timeout()` | `Promise<PrimerState>` |
|
|
337
|
+
| `MatchState` | `submitMatch(pairs)` or `timeout()` | `Promise<PrimerState>` |
|
|
338
|
+
| `PciInteractionState` | `submit(value)` or `timeout()` | `Promise<PrimerState>` |
|
|
339
|
+
| `FeedbackState` | `advance()` | `Promise<PrimerState>` |
|
|
340
|
+
| `CompletedState` | none | terminal |
|
|
341
|
+
| `ErroredState` | `retry()` when `retriable` is `true` | `Promise<PrimerState>` |
|
|
342
|
+
| `FatalState` | none | terminal |
|
|
343
|
+
|
|
344
|
+
## `PrimerState`
|
|
177
345
|
|
|
178
346
|
```ts
|
|
179
347
|
type PrimerState<Pcis extends PciId = PciId> =
|
|
@@ -185,168 +353,902 @@ type PrimerState<Pcis extends PciId = PciId> =
|
|
|
185
353
|
| FatalState
|
|
186
354
|
```
|
|
187
355
|
|
|
188
|
-
|
|
356
|
+
`PrimerState` is live in-memory state. It contains transition closures, pending-operation guards, and retry behavior. Do not serialize it, store it, clone it through JSON, or pass it through host data. Calling `JSON.stringify(state)` throws `ErrNotSerializable`.
|
|
189
357
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
358
|
+
Recreate state by calling `create` again after a reload, remount, account switch, or subject switch.
|
|
359
|
+
|
|
360
|
+
## Common State Fields
|
|
361
|
+
|
|
362
|
+
Learning states that render content expose `body` and `stimulus`.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
interface RenderableState {
|
|
366
|
+
readonly body: ContentBlock[]
|
|
367
|
+
readonly stimulus: RendererStimulus | null
|
|
368
|
+
}
|
|
201
369
|
```
|
|
202
370
|
|
|
203
|
-
|
|
371
|
+
`body` is the main instructional content. `stimulus` is optional supporting material. Current stimulus support is image-only, but it is still a discriminated union so renderers can remain future-safe.
|
|
204
372
|
|
|
205
|
-
##
|
|
373
|
+
## `ObservationState`
|
|
206
374
|
|
|
207
375
|
```ts
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
376
|
+
interface ObservationState<Pcis extends PciId = PciId> {
|
|
377
|
+
readonly phase: "observation"
|
|
378
|
+
readonly body: ContentBlock[]
|
|
379
|
+
readonly stimulus: RendererStimulus | null
|
|
380
|
+
advance(): Promise<PrimerState<Pcis>>
|
|
381
|
+
}
|
|
382
|
+
```
|
|
215
383
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
384
|
+
Render the frame, then call `advance()` when the learner is ready to continue. Observation states have no answer to submit.
|
|
385
|
+
|
|
386
|
+
Repeated `advance()` calls while the first one is pending return the same pending result.
|
|
387
|
+
|
|
388
|
+
## `InteractionState`
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
type InteractionState<Pcis extends PciId = PciId> =
|
|
392
|
+
| ChoiceState<Pcis>
|
|
393
|
+
| TextEntryState<Pcis>
|
|
394
|
+
| ExtendedTextState<Pcis>
|
|
395
|
+
| OrderState<Pcis>
|
|
396
|
+
| MatchState<Pcis>
|
|
397
|
+
| PciInteractionState<Pcis>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Every interaction state includes:
|
|
401
|
+
|
|
402
|
+
| Field | Meaning |
|
|
403
|
+
| --- | --- |
|
|
404
|
+
| `phase: "interaction"` | State-machine discriminator. |
|
|
405
|
+
| `kind` | Renderer-facing interaction kind. |
|
|
406
|
+
| `body` | Frame content. |
|
|
407
|
+
| `stimulus` | Optional frame stimulus. |
|
|
408
|
+
| `interaction` | Full interaction contract object. |
|
|
409
|
+
| submit method | Kind-specific learner submission operation. |
|
|
410
|
+
| `timeout()` | Records that the learner timed out or the host chose to end the attempt without a submission. |
|
|
411
|
+
|
|
412
|
+
Submission methods validate standard interaction payloads before runtime submission. Invalid standard submissions return `ErroredState` with `ErrInvalidSubmission`.
|
|
413
|
+
|
|
414
|
+
Concurrent interaction operations are guarded:
|
|
415
|
+
|
|
416
|
+
| Situation | SDK behavior |
|
|
417
|
+
| --- | --- |
|
|
418
|
+
| Same submit payload while submit is pending | Returns the same pending result. |
|
|
419
|
+
| Different submit payload while submit is pending | Returns `ErroredState` with `ErrConflict`. |
|
|
420
|
+
| Submit while timeout is pending | Returns `ErroredState` with `ErrConflict`. |
|
|
421
|
+
| Timeout while submit is pending | Returns `ErroredState` with `ErrConflict`. |
|
|
422
|
+
| Repeated timeout while timeout is pending | Returns the same pending result. |
|
|
423
|
+
|
|
424
|
+
## `ChoiceState`
|
|
425
|
+
|
|
426
|
+
```ts
|
|
427
|
+
interface ChoiceState<Pcis extends PciId = PciId> {
|
|
428
|
+
readonly phase: "interaction"
|
|
429
|
+
readonly kind: "choice"
|
|
430
|
+
readonly body: ContentBlock[]
|
|
431
|
+
readonly stimulus: RendererStimulus | null
|
|
432
|
+
readonly interaction: Extract<StandardRendererInteraction, { type: "choice" }>
|
|
433
|
+
readonly options: RendererChoice[]
|
|
434
|
+
readonly minChoices: number
|
|
435
|
+
readonly maxChoices: number
|
|
436
|
+
submitChoice(selectedKeys: string[]): Promise<PrimerState<Pcis>>
|
|
437
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
236
438
|
}
|
|
439
|
+
```
|
|
237
440
|
|
|
238
|
-
|
|
239
|
-
|
|
441
|
+
Use `minChoices` and `maxChoices` to decide whether the UI should submit immediately or require an explicit submit action.
|
|
442
|
+
|
|
443
|
+
Valid `submitChoice` payloads:
|
|
444
|
+
|
|
445
|
+
| Requirement | Error if violated |
|
|
446
|
+
| --- | --- |
|
|
447
|
+
| At least `minChoices` identifiers | `ErrInvalidSubmission` |
|
|
448
|
+
| At most `maxChoices` identifiers | `ErrInvalidSubmission` |
|
|
449
|
+
| Every identifier exists in `options` | `ErrInvalidSubmission` |
|
|
450
|
+
| No duplicate identifiers | `ErrInvalidSubmission` |
|
|
451
|
+
|
|
452
|
+
## `TextEntryState`
|
|
453
|
+
|
|
454
|
+
```ts
|
|
455
|
+
interface TextEntryState<Pcis extends PciId = PciId> {
|
|
456
|
+
readonly phase: "interaction"
|
|
457
|
+
readonly kind: "text-entry"
|
|
458
|
+
readonly body: ContentBlock[]
|
|
459
|
+
readonly stimulus: RendererStimulus | null
|
|
460
|
+
readonly interaction: Extract<StandardRendererInteraction, { type: "text-entry" }>
|
|
461
|
+
submitText(value: string): Promise<PrimerState<Pcis>>
|
|
462
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
463
|
+
}
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
The interaction may include `expectedLength`, `patternMask`, and `placeholderText`. These are renderer hints. The SDK requires the submission to be a text-entry submission with a string value.
|
|
467
|
+
|
|
468
|
+
## `ExtendedTextState`
|
|
469
|
+
|
|
470
|
+
Extended text has two cardinalities.
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
interface ExtendedTextSingleState<Pcis extends PciId = PciId> {
|
|
474
|
+
readonly phase: "interaction"
|
|
475
|
+
readonly kind: "extended-text"
|
|
476
|
+
readonly cardinality: "single"
|
|
477
|
+
readonly body: ContentBlock[]
|
|
478
|
+
readonly stimulus: RendererStimulus | null
|
|
479
|
+
readonly interaction: Extract<
|
|
480
|
+
StandardRendererInteraction,
|
|
481
|
+
{ type: "extended-text"; cardinality: "single" }
|
|
482
|
+
>
|
|
483
|
+
submitText(value: string): Promise<PrimerState<Pcis>>
|
|
484
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
interface ExtendedTextMultipleState<Pcis extends PciId = PciId> {
|
|
488
|
+
readonly phase: "interaction"
|
|
489
|
+
readonly kind: "extended-text"
|
|
490
|
+
readonly cardinality: "multiple"
|
|
491
|
+
readonly body: ContentBlock[]
|
|
492
|
+
readonly stimulus: RendererStimulus | null
|
|
493
|
+
readonly interaction: Extract<
|
|
494
|
+
StandardRendererInteraction,
|
|
495
|
+
{ type: "extended-text"; cardinality: "multiple" }
|
|
496
|
+
>
|
|
497
|
+
readonly minStrings: number
|
|
498
|
+
readonly maxStrings: number
|
|
499
|
+
submitTexts(values: string[]): Promise<PrimerState<Pcis>>
|
|
500
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
For single-cardinality extended text, call `submitText(value)`. For multiple-cardinality extended text, call `submitTexts(values)`.
|
|
505
|
+
|
|
506
|
+
Valid multiple-cardinality payloads:
|
|
507
|
+
|
|
508
|
+
| Requirement | Error if violated |
|
|
509
|
+
| --- | --- |
|
|
510
|
+
| At least `minStrings` values | `ErrInvalidSubmission` |
|
|
511
|
+
| At most `maxStrings` values | `ErrInvalidSubmission` |
|
|
512
|
+
| No duplicate values | `ErrInvalidSubmission` |
|
|
513
|
+
|
|
514
|
+
`expectedLength`, `expectedLines`, `patternMask`, and `placeholderText` are renderer hints.
|
|
515
|
+
|
|
516
|
+
## `OrderState`
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
interface OrderState<Pcis extends PciId = PciId> {
|
|
520
|
+
readonly phase: "interaction"
|
|
521
|
+
readonly kind: "order"
|
|
522
|
+
readonly body: ContentBlock[]
|
|
523
|
+
readonly stimulus: RendererStimulus | null
|
|
524
|
+
readonly interaction: Extract<StandardRendererInteraction, { type: "order" }>
|
|
525
|
+
readonly choices: RendererChoice[]
|
|
526
|
+
readonly minChoices: number
|
|
527
|
+
readonly maxChoices: number
|
|
528
|
+
submitOrder(orderedKeys: string[]): Promise<PrimerState<Pcis>>
|
|
529
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
240
530
|
}
|
|
241
531
|
```
|
|
242
532
|
|
|
243
|
-
|
|
533
|
+
Submit identifiers in learner-selected order.
|
|
244
534
|
|
|
245
|
-
|
|
535
|
+
Valid `submitOrder` payloads:
|
|
536
|
+
|
|
537
|
+
| Requirement | Error if violated |
|
|
538
|
+
| --- | --- |
|
|
539
|
+
| At least `minChoices` identifiers | `ErrInvalidSubmission` |
|
|
540
|
+
| At most `maxChoices` identifiers | `ErrInvalidSubmission` |
|
|
541
|
+
| Every identifier exists in `choices` | `ErrInvalidSubmission` |
|
|
542
|
+
| No duplicate identifiers | `ErrInvalidSubmission` |
|
|
543
|
+
|
|
544
|
+
## `MatchState`
|
|
246
545
|
|
|
247
546
|
```ts
|
|
248
|
-
|
|
547
|
+
interface MatchPair {
|
|
548
|
+
source: string
|
|
549
|
+
target: string
|
|
550
|
+
}
|
|
249
551
|
|
|
250
|
-
|
|
251
|
-
|
|
552
|
+
interface MatchState<Pcis extends PciId = PciId> {
|
|
553
|
+
readonly phase: "interaction"
|
|
554
|
+
readonly kind: "match"
|
|
555
|
+
readonly body: ContentBlock[]
|
|
556
|
+
readonly stimulus: RendererStimulus | null
|
|
557
|
+
readonly interaction: Extract<StandardRendererInteraction, { type: "match" }>
|
|
558
|
+
readonly sourceChoices: RendererMatchChoice[]
|
|
559
|
+
readonly targetChoices: RendererMatchChoice[]
|
|
560
|
+
readonly minAssociations: number
|
|
561
|
+
readonly maxAssociations: number
|
|
562
|
+
submitMatch(pairs: MatchPair[]): Promise<PrimerState<Pcis>>
|
|
563
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Each pair connects one source identifier to one target identifier.
|
|
568
|
+
|
|
569
|
+
Valid `submitMatch` payloads:
|
|
570
|
+
|
|
571
|
+
| Requirement | Error if violated |
|
|
572
|
+
| --- | --- |
|
|
573
|
+
| At least `minAssociations` pairs | `ErrInvalidSubmission` |
|
|
574
|
+
| At most `maxAssociations` pairs | `ErrInvalidSubmission` |
|
|
575
|
+
| Every source identifier exists in `sourceChoices` | `ErrInvalidSubmission` |
|
|
576
|
+
| Every target identifier exists in `targetChoices` | `ErrInvalidSubmission` |
|
|
577
|
+
| No duplicate source-target pairs | `ErrInvalidSubmission` |
|
|
578
|
+
| Every choice respects its `matchMin` | `ErrInvalidSubmission` |
|
|
579
|
+
| Every choice respects its `matchMax` when `matchMax` is nonzero | `ErrInvalidSubmission` |
|
|
580
|
+
|
|
581
|
+
`matchMax: 0` means unbounded.
|
|
582
|
+
|
|
583
|
+
## `PciInteractionState`
|
|
584
|
+
|
|
585
|
+
Portable Custom Interaction state is typed by PCI id.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
type PciInteractionState<Pcis extends PciId = PciId> = {
|
|
589
|
+
[K in Pcis]: {
|
|
590
|
+
readonly phase: "interaction"
|
|
591
|
+
readonly kind: "portable-custom"
|
|
592
|
+
readonly body: ContentBlock[]
|
|
593
|
+
readonly stimulus: RendererStimulus | null
|
|
594
|
+
readonly interaction: PciInteraction<K>
|
|
595
|
+
readonly pciId: K
|
|
596
|
+
readonly properties: PciProps<K>
|
|
597
|
+
submit(value: PciValue<K>): Promise<PrimerState<Pcis>>
|
|
598
|
+
timeout(): Promise<PrimerState<Pcis>>
|
|
599
|
+
}
|
|
600
|
+
}[Pcis]
|
|
252
601
|
```
|
|
253
602
|
|
|
254
603
|
When the state is narrowed to a PCI id, `submit` accepts only that PCI's value type.
|
|
255
604
|
|
|
256
605
|
```ts
|
|
606
|
+
import type { PciValue } from "@superbuilders/primer-tives/contracts"
|
|
607
|
+
|
|
257
608
|
if (state.phase === "interaction" && state.kind === "portable-custom") {
|
|
258
609
|
if (state.pciId === "urn:primer:pci:fraction-input") {
|
|
259
|
-
const value = readFractionInput(
|
|
610
|
+
const value: PciValue<"urn:primer:pci:fraction-input"> = readFractionInput(
|
|
611
|
+
state.properties
|
|
612
|
+
)
|
|
260
613
|
state = await state.submit(value)
|
|
261
614
|
}
|
|
262
615
|
}
|
|
263
616
|
```
|
|
264
617
|
|
|
265
|
-
##
|
|
618
|
+
## `FeedbackState`
|
|
266
619
|
|
|
267
|
-
|
|
620
|
+
```ts
|
|
621
|
+
interface FeedbackState<Pcis extends PciId = PciId> {
|
|
622
|
+
readonly phase: "feedback"
|
|
623
|
+
readonly body: ContentBlock[]
|
|
624
|
+
readonly stimulus: RendererStimulus | null
|
|
625
|
+
readonly interaction: RendererInteraction<Pcis>
|
|
626
|
+
readonly submission: RendererSubmission<Pcis>
|
|
627
|
+
readonly isCorrect: boolean
|
|
628
|
+
readonly feedbackContent: ContentInline[]
|
|
629
|
+
readonly review: InteractionReview<Pcis> | null
|
|
630
|
+
advance(): Promise<PrimerState<Pcis>>
|
|
631
|
+
}
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
Feedback state is returned after a successful learner submission. Render the submitted frame, submitted value, correctness, feedback content, and optional review data. Call `advance()` when the learner is ready to continue.
|
|
635
|
+
|
|
636
|
+
Repeated `advance()` calls while the first one is pending return the same pending result.
|
|
637
|
+
|
|
638
|
+
## `CompletedState`
|
|
268
639
|
|
|
269
640
|
```ts
|
|
270
|
-
|
|
641
|
+
interface CompletedState {
|
|
642
|
+
readonly phase: "completed"
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
Terminal state. There is no transition method.
|
|
271
647
|
|
|
272
|
-
|
|
648
|
+
## `ErroredState`
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
interface ErroredState<Pcis extends PciId = PciId> {
|
|
652
|
+
readonly phase: "errored"
|
|
653
|
+
readonly error: Error
|
|
654
|
+
readonly retriable: boolean
|
|
655
|
+
retry(): Promise<PrimerState<Pcis>>
|
|
656
|
+
}
|
|
273
657
|
```
|
|
274
658
|
|
|
275
|
-
`
|
|
659
|
+
`ErroredState` means the current learner intent could not complete, but the learning session itself is not necessarily terminal.
|
|
276
660
|
|
|
277
|
-
|
|
661
|
+
If `retriable` is `true`, `retry()` repeats the exact failed intent. If `retriable` is `false`, `retry()` resolves to the same errored state.
|
|
278
662
|
|
|
279
|
-
|
|
663
|
+
`ErrInvalidSubmission` is non-retriable because the submitted value was invalid for the active interaction. Renderer code should fix the payload before submitting again from a fresh interaction state.
|
|
280
664
|
|
|
281
|
-
|
|
665
|
+
## `FatalState`
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
interface FatalState {
|
|
669
|
+
readonly phase: "fatal"
|
|
670
|
+
readonly error: Error
|
|
671
|
+
readonly retriable: false
|
|
672
|
+
}
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
Fatal state means the SDK cannot recover by retrying the current learner intent. Render a terminal error UI, require a new `create` call, or send the learner through auth again depending on the sentinel.
|
|
676
|
+
|
|
677
|
+
Fatal sentinels:
|
|
282
678
|
|
|
283
679
|
| Sentinel | Meaning |
|
|
284
680
|
| --- | --- |
|
|
285
|
-
| `
|
|
286
|
-
| `
|
|
287
|
-
| `
|
|
288
|
-
| `
|
|
289
|
-
| `
|
|
290
|
-
| `
|
|
291
|
-
| `
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
681
|
+
| `ErrBadRequest` | Primer rejected the runtime request as invalid for the SDK contract. |
|
|
682
|
+
| `ErrInvalidAccessToken` | The learner token was rejected. |
|
|
683
|
+
| `ErrTokenExpired` | The learner token expired. |
|
|
684
|
+
| `ErrForbidden` | The learner is not allowed to continue in this runtime scope. |
|
|
685
|
+
| `ErrNotFound` | The requested runtime scope or state could not be found. |
|
|
686
|
+
| `ErrSdkUpgradeRequired` | The installed SDK is too old for the current Primer runtime. |
|
|
687
|
+
| `ErrUnsupportedPci` | Primer presented a PCI that the renderer did not declare in `supportedPcis`. |
|
|
688
|
+
|
|
689
|
+
## `/contracts`
|
|
690
|
+
|
|
691
|
+
The contracts subpath contains renderer-facing data types and validation helpers.
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
import {
|
|
695
|
+
ChoiceSubmissionSchema,
|
|
696
|
+
ExtendedTextSubmissionSchema,
|
|
697
|
+
FractionInputPciSubmissionSchema,
|
|
698
|
+
MatchPairSchema,
|
|
699
|
+
MatchSubmissionSchema,
|
|
700
|
+
OrderSubmissionSchema,
|
|
701
|
+
PCI_IDS,
|
|
702
|
+
RendererSubmissionSchema,
|
|
703
|
+
TextEntrySubmissionSchema,
|
|
704
|
+
blocksToPlainText,
|
|
705
|
+
inlinesToPlainText,
|
|
706
|
+
isPciId,
|
|
707
|
+
submissionValidationMessage,
|
|
708
|
+
validateSubmissionForInteraction
|
|
709
|
+
} from "@superbuilders/primer-tives/contracts"
|
|
710
|
+
|
|
711
|
+
import type {
|
|
712
|
+
ContentBlock,
|
|
713
|
+
ContentInline,
|
|
714
|
+
ContentSpan,
|
|
715
|
+
FractionInputForm,
|
|
716
|
+
FractionInputProps,
|
|
717
|
+
FractionInputSubmission,
|
|
718
|
+
ImageStimulus,
|
|
719
|
+
InteractionReview,
|
|
720
|
+
MatchPair,
|
|
721
|
+
PciId,
|
|
722
|
+
PciInteraction,
|
|
723
|
+
PciProps,
|
|
724
|
+
PciRegistry,
|
|
725
|
+
PciSubmission,
|
|
726
|
+
PciUrn,
|
|
727
|
+
PciValue,
|
|
728
|
+
RendererChoice,
|
|
729
|
+
RendererInteraction,
|
|
730
|
+
RendererMatchChoice,
|
|
731
|
+
RendererStimulus,
|
|
732
|
+
RendererSubmission,
|
|
733
|
+
StandardRendererInteraction
|
|
734
|
+
} from "@superbuilders/primer-tives/contracts"
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
## Content
|
|
738
|
+
|
|
739
|
+
```ts
|
|
740
|
+
type ContentSpan = { type: "text"; value: string } | { type: "italic"; value: string }
|
|
741
|
+
type ContentInline = ContentSpan | { type: "latex"; value: string }
|
|
742
|
+
type ContentBlock = { type: "paragraph"; children: ContentInline[] }
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
Helpers:
|
|
746
|
+
|
|
747
|
+
```ts
|
|
748
|
+
function inlinesToPlainText(nodes: ContentInline[]): string
|
|
749
|
+
function blocksToPlainText(blocks: ContentBlock[]): string
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
Use the plain-text helpers for accessibility labels, logging summaries, search snippets, and renderer fallbacks. LaTeX inline nodes contribute their raw `value` to plain text.
|
|
753
|
+
|
|
754
|
+
## Stimulus
|
|
755
|
+
|
|
756
|
+
```ts
|
|
757
|
+
interface ImageStimulus {
|
|
758
|
+
kind: "image"
|
|
759
|
+
alt: ContentInline[]
|
|
760
|
+
src: string
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
type RendererStimulus = ImageStimulus
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
`RendererStimulus` is currently image-only. Always switch on `stimulus.kind` anyway.
|
|
767
|
+
|
|
768
|
+
## Interactions
|
|
769
|
+
|
|
770
|
+
```ts
|
|
771
|
+
type RendererInteraction<Pcis extends PciId = PciId> =
|
|
772
|
+
| StandardRendererInteraction
|
|
773
|
+
| PciInteraction<Pcis>
|
|
774
|
+
```
|
|
775
|
+
|
|
776
|
+
Standard interactions:
|
|
777
|
+
|
|
778
|
+
| Type | Key fields |
|
|
779
|
+
| --- | --- |
|
|
780
|
+
| `choice` | `prompt`, `options`, `shuffle`, `minChoices`, `maxChoices` |
|
|
781
|
+
| `text-entry` | `prompt`, `base`, `expectedLength`, `patternMask`, `placeholderText` |
|
|
782
|
+
| `extended-text` single | `prompt`, `format`, `expectedLines`, `expectedLength`, `patternMask`, `placeholderText` |
|
|
783
|
+
| `extended-text` multiple | single fields plus `minStrings`, `maxStrings` |
|
|
784
|
+
| `order` | `prompt`, `choices`, `shuffle`, `minChoices`, `maxChoices` |
|
|
785
|
+
| `match` | `prompt`, `sourceChoices`, `targetChoices`, `shuffle`, `minAssociations`, `maxAssociations` |
|
|
786
|
+
| `portable-custom` | `prompt`, `pciId`, `properties` |
|
|
787
|
+
|
|
788
|
+
Choice objects:
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
interface RendererChoice {
|
|
792
|
+
identifier: string
|
|
793
|
+
content: ContentInline[]
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
Match choice objects:
|
|
798
|
+
|
|
799
|
+
```ts
|
|
800
|
+
interface RendererMatchChoice {
|
|
801
|
+
identifier: string
|
|
802
|
+
content: ContentInline[]
|
|
803
|
+
matchMax: number
|
|
804
|
+
matchMin: number
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
## Submissions
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
type RendererSubmission<Pcis extends PciId = PciId> =
|
|
812
|
+
| { type: "choice"; selectedKeys: string[] }
|
|
813
|
+
| { type: "text-entry"; value: string }
|
|
814
|
+
| { type: "extended-text"; values: string[] }
|
|
815
|
+
| { type: "order"; orderedKeys: string[] }
|
|
816
|
+
| { type: "match"; pairs: MatchPair[] }
|
|
817
|
+
| PciSubmission<Pcis>
|
|
818
|
+
```
|
|
819
|
+
|
|
820
|
+
Public schemas:
|
|
821
|
+
|
|
822
|
+
| Schema | Validates |
|
|
823
|
+
| --- | --- |
|
|
824
|
+
| `MatchPairSchema` | `{ source, target }` match pair shape |
|
|
825
|
+
| `ChoiceSubmissionSchema` | choice submission shape |
|
|
826
|
+
| `TextEntrySubmissionSchema` | text-entry submission shape |
|
|
827
|
+
| `ExtendedTextSubmissionSchema` | extended-text submission shape |
|
|
828
|
+
| `OrderSubmissionSchema` | order submission shape |
|
|
829
|
+
| `MatchSubmissionSchema` | match submission shape |
|
|
830
|
+
| `FractionInputPciSubmissionSchema` | fraction-input PCI submission shape |
|
|
831
|
+
| `RendererSubmissionSchema` | union of all supported submission shapes |
|
|
832
|
+
|
|
833
|
+
Always use `safeParse` when parsing arbitrary input.
|
|
300
834
|
|
|
301
835
|
```ts
|
|
302
836
|
import * as errors from "@superbuilders/errors"
|
|
303
|
-
import
|
|
837
|
+
import * as logger from "@superbuilders/slog"
|
|
838
|
+
import { RendererSubmissionSchema } from "@superbuilders/primer-tives/contracts"
|
|
304
839
|
|
|
305
|
-
const
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
840
|
+
const parsed = RendererSubmissionSchema.safeParse(payload)
|
|
841
|
+
if (!parsed.success) {
|
|
842
|
+
logger.error("submission payload invalid", { error: parsed.error })
|
|
843
|
+
throw errors.wrap(parsed.error, "submission payload")
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const submission = parsed.data
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
## Semantic Submission Validation
|
|
850
|
+
|
|
851
|
+
Shape validation answers “does this look like a submission?” Semantic validation answers “is this submission valid for this exact interaction?”
|
|
852
|
+
|
|
853
|
+
```ts
|
|
854
|
+
function validateSubmissionForInteraction(
|
|
855
|
+
interaction: RendererInteraction,
|
|
856
|
+
submission: RendererSubmission
|
|
857
|
+
): SubmissionValidationResult
|
|
858
|
+
|
|
859
|
+
function submissionValidationMessage(result: SubmissionValidationFailure): string
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
Result shape:
|
|
863
|
+
|
|
864
|
+
```ts
|
|
865
|
+
type SubmissionValidationResult =
|
|
866
|
+
| { ok: true; value: RendererSubmission }
|
|
867
|
+
| { ok: false; issues: readonly string[] }
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
Validation checks:
|
|
871
|
+
|
|
872
|
+
| Interaction | Checks |
|
|
873
|
+
| --- | --- |
|
|
874
|
+
| `choice` | type match, min/max selection count, duplicate identifiers, unknown identifiers |
|
|
875
|
+
| `text-entry` | type match |
|
|
876
|
+
| `extended-text` single | type match, exactly one value |
|
|
877
|
+
| `extended-text` multiple | type match, min/max value count, duplicate values |
|
|
878
|
+
| `order` | type match, min/max selection count, duplicate identifiers, unknown identifiers |
|
|
879
|
+
| `match` | type match, min/max association count, duplicate pairs, unknown sources, unknown targets, `matchMin`, `matchMax` |
|
|
880
|
+
| `portable-custom` | type match, PCI id match, PCI value schema |
|
|
881
|
+
|
|
882
|
+
The built-in standard interaction state methods call this before submitting. Custom renderer utilities that build `RendererSubmission` values directly should call it too.
|
|
883
|
+
|
|
884
|
+
```ts
|
|
885
|
+
import * as errors from "@superbuilders/errors"
|
|
886
|
+
import * as logger from "@superbuilders/slog"
|
|
887
|
+
import {
|
|
888
|
+
submissionValidationMessage,
|
|
889
|
+
validateSubmissionForInteraction
|
|
890
|
+
} from "@superbuilders/primer-tives/contracts"
|
|
891
|
+
import { ErrInvalidSubmission } from "@superbuilders/primer-tives/errors"
|
|
892
|
+
|
|
893
|
+
const validation = validateSubmissionForInteraction(interaction, submission)
|
|
894
|
+
if (!validation.ok) {
|
|
895
|
+
const message = submissionValidationMessage(validation)
|
|
896
|
+
logger.error("submission invalid", { issues: validation.issues })
|
|
897
|
+
throw errors.wrap(ErrInvalidSubmission, message)
|
|
898
|
+
}
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
## Review Types
|
|
902
|
+
|
|
903
|
+
`FeedbackState.review` is `InteractionReview | null`.
|
|
904
|
+
|
|
905
|
+
```ts
|
|
906
|
+
type InteractionReview<Pcis extends PciId = PciId> =
|
|
907
|
+
| ChoiceReview
|
|
908
|
+
| TextEntryReview
|
|
909
|
+
| ExtendedTextReview
|
|
910
|
+
| OrderReview
|
|
911
|
+
| MatchReview
|
|
912
|
+
| PciReview<Pcis>
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Review variants:
|
|
916
|
+
|
|
917
|
+
| Type | Data |
|
|
918
|
+
| --- | --- |
|
|
919
|
+
| `choice` | `correctKeys: string[]` |
|
|
920
|
+
| `text-entry` | `correctValue: ReviewScalarValue | null` |
|
|
921
|
+
| `extended-text` | `correctValues: ReviewScalarValue[]` |
|
|
922
|
+
| `order` | `correctOrder: string[]` |
|
|
923
|
+
| `match` | `correctPairs: MatchPair[]` |
|
|
924
|
+
| `portable-custom` | `pciId`, `fields: ReviewRecordField[]` |
|
|
925
|
+
|
|
926
|
+
Review scalar values:
|
|
927
|
+
|
|
928
|
+
```ts
|
|
929
|
+
type ReviewScalarValue =
|
|
930
|
+
| { kind: "identifier"; value: string }
|
|
931
|
+
| { kind: "string"; value: string }
|
|
932
|
+
| { kind: "integer"; value: number }
|
|
933
|
+
| { kind: "float"; value: number }
|
|
934
|
+
| { kind: "pair"; source: string; target: string }
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
`review` is for renderer display and inspection. Correctness already lives on `FeedbackState.isCorrect`.
|
|
938
|
+
|
|
939
|
+
## PCI Registry
|
|
940
|
+
|
|
941
|
+
The current PCI registry contains one PCI.
|
|
942
|
+
|
|
943
|
+
```ts
|
|
944
|
+
const PCI_IDS = ["urn:primer:pci:fraction-input"] as const
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
```ts
|
|
948
|
+
type PciId = "urn:primer:pci:fraction-input"
|
|
949
|
+
type PciProps<K extends PciId> = PciRegistry[K]["props"]
|
|
950
|
+
type PciValue<K extends PciId> = PciRegistry[K]["value"]
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
Use `isPciId(value)` to narrow an arbitrary string to the current `PciId` union.
|
|
954
|
+
|
|
955
|
+
## Fraction Input PCI
|
|
956
|
+
|
|
957
|
+
```ts
|
|
958
|
+
type FractionInputForm = "whole" | "proper" | "improper" | "mixed"
|
|
959
|
+
|
|
960
|
+
interface FractionInputProps {
|
|
961
|
+
form: FractionInputForm
|
|
962
|
+
requireSimplified: boolean
|
|
963
|
+
}
|
|
964
|
+
```
|
|
965
|
+
|
|
966
|
+
Submitted value:
|
|
967
|
+
|
|
968
|
+
```ts
|
|
969
|
+
type FractionInputSubmission =
|
|
970
|
+
| { form: "whole"; whole: string }
|
|
971
|
+
| { form: "proper"; numerator: string; denominator: string }
|
|
972
|
+
| { form: "improper"; numerator: string; denominator: string }
|
|
973
|
+
| { form: "mixed"; whole: string; numerator: string; denominator: string }
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
Example renderer branch:
|
|
977
|
+
|
|
978
|
+
```ts
|
|
979
|
+
if (state.phase === "interaction" && state.kind === "portable-custom") {
|
|
980
|
+
if (state.pciId === "urn:primer:pci:fraction-input") {
|
|
981
|
+
renderFractionInput({
|
|
982
|
+
mode: "pending",
|
|
983
|
+
properties: state.properties,
|
|
984
|
+
onValueChange: handleFractionValueChange
|
|
985
|
+
})
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
```
|
|
989
|
+
|
|
990
|
+
## PCI Render Props
|
|
991
|
+
|
|
992
|
+
PCI render props are convenience types for renderer components.
|
|
993
|
+
|
|
994
|
+
```ts
|
|
995
|
+
type PciPendingRenderProps<K extends PciId> = {
|
|
996
|
+
mode: "pending"
|
|
997
|
+
properties: PciProps<K>
|
|
998
|
+
onValueChange: (value: PciValue<K> | null) => void
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
type PciSubmittedRenderProps<K extends PciId> = {
|
|
1002
|
+
mode: "submitted"
|
|
1003
|
+
properties: PciProps<K>
|
|
1004
|
+
submission: PciValue<K>
|
|
1005
|
+
review: Extract<InteractionReview<K>, { type: "portable-custom"; pciId: K }> | null
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
type PciRenderProps<K extends PciId> = PciPendingRenderProps<K> | PciSubmittedRenderProps<K>
|
|
1009
|
+
```
|
|
1010
|
+
|
|
1011
|
+
## Subject PCI Helpers
|
|
1012
|
+
|
|
1013
|
+
Use `@superbuilders/primer-tives/subject-pcis` when renderer tooling needs the same subject-to-PCI contract as `create`.
|
|
1014
|
+
|
|
1015
|
+
```ts
|
|
1016
|
+
import {
|
|
1017
|
+
REQUIRED_PCIS_BY_SUBJECT,
|
|
1018
|
+
missingPcisForSubject,
|
|
1019
|
+
requiredPcisForSubject
|
|
1020
|
+
} from "@superbuilders/primer-tives/subject-pcis"
|
|
1021
|
+
import type {
|
|
1022
|
+
HasRequiredPcis,
|
|
1023
|
+
MissingRequiredPcis,
|
|
1024
|
+
RequiredPciForSubject
|
|
1025
|
+
} from "@superbuilders/primer-tives/subject-pcis"
|
|
1026
|
+
|
|
1027
|
+
const requiredForMath = requiredPcisForSubject("math")
|
|
1028
|
+
const missingForMath = missingPcisForSubject("math", [])
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
`requiredPcisForSubject(undefined)` returns the all-subject required PCI union.
|
|
1032
|
+
|
|
1033
|
+
## Errors
|
|
1034
|
+
|
|
1035
|
+
All SDK sentinels are exported from `@superbuilders/primer-tives/errors` and are compatible with `errors.is()` from `@superbuilders/errors`.
|
|
1036
|
+
|
|
1037
|
+
```ts
|
|
1038
|
+
import * as errors from "@superbuilders/errors"
|
|
1039
|
+
import { ErrTokenExpired } from "@superbuilders/primer-tives/errors"
|
|
1040
|
+
|
|
1041
|
+
if (errors.is(err, ErrTokenExpired)) {
|
|
1042
|
+
renderSignInAgain()
|
|
1043
|
+
}
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
Complete export set:
|
|
1047
|
+
|
|
1048
|
+
```ts
|
|
1049
|
+
import {
|
|
1050
|
+
ErrAuthCallbackInvalid,
|
|
1051
|
+
ErrAuthCancelled,
|
|
1052
|
+
ErrAuthConfigInvalid,
|
|
1053
|
+
ErrAuthPopupBlocked,
|
|
1054
|
+
ErrAuthStateMismatch,
|
|
1055
|
+
ErrAuthUnavailable,
|
|
1056
|
+
ErrBadRequest,
|
|
1057
|
+
ErrConflict,
|
|
1058
|
+
ErrForbidden,
|
|
1059
|
+
ErrInvalidAccessToken,
|
|
1060
|
+
ErrInvalidSubmission,
|
|
1061
|
+
ErrJsonParse,
|
|
1062
|
+
ErrMalformedAccessToken,
|
|
1063
|
+
ErrNetwork,
|
|
1064
|
+
ErrNotFound,
|
|
1065
|
+
ErrNotSerializable,
|
|
1066
|
+
ErrRateLimited,
|
|
1067
|
+
ErrSdkUpgradeRequired,
|
|
1068
|
+
ErrServerError,
|
|
1069
|
+
ErrServiceUnavailable,
|
|
1070
|
+
ErrTimeout,
|
|
1071
|
+
ErrTokenExpired,
|
|
1072
|
+
ErrUnsupportedPci
|
|
1073
|
+
} from "@superbuilders/primer-tives/errors"
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
## Create-Time Errors
|
|
1077
|
+
|
|
1078
|
+
These are most often thrown by `create` before a `PrimerState` exists.
|
|
1079
|
+
|
|
1080
|
+
| Sentinel | Meaning | Typical handling |
|
|
1081
|
+
| --- | --- | --- |
|
|
1082
|
+
| `ErrAuthUnavailable` | SDK-managed auth cannot run in the current host environment. | Show unsupported-runtime or fallback sign-in UI. |
|
|
1083
|
+
| `ErrAuthConfigInvalid` | Public auth configuration is invalid. | Log and treat as integration error. |
|
|
1084
|
+
| `ErrAuthCallbackInvalid` | Learner auth did not complete with an acceptable result. | Offer sign-in retry; log if unexpected. |
|
|
1085
|
+
| `ErrAuthStateMismatch` | Auth result does not match the initiated auth attempt. | Restart auth. |
|
|
1086
|
+
| `ErrAuthPopupBlocked` | Browser blocked learner auth UI. | Ask learner to allow popups or retry from a user gesture. |
|
|
1087
|
+
| `ErrAuthCancelled` | Learner auth was closed or timed out. | Offer retry. |
|
|
1088
|
+
| `ErrMalformedAccessToken` | Provided or resolved token is not shaped like a learner access token. | Re-authenticate learner or fix token source. |
|
|
1089
|
+
|
|
1090
|
+
## Runtime Error States
|
|
1091
|
+
|
|
1092
|
+
Runtime errors are represented as `ErroredState` or `FatalState`.
|
|
1093
|
+
|
|
1094
|
+
| Sentinel | State | Retriable | Meaning |
|
|
1095
|
+
| --- | --- | --- | --- |
|
|
1096
|
+
| `ErrNetwork` | `ErroredState` | yes | Runtime communication failed before a usable Primer result existed. |
|
|
1097
|
+
| `ErrTimeout` | `ErroredState` | yes | Runtime work was aborted or exceeded the host's allowed time. |
|
|
1098
|
+
| `ErrServerError` | `ErroredState` | yes | Primer could not produce a normal runtime result. |
|
|
1099
|
+
| `ErrServiceUnavailable` | `ErroredState` | yes | Primer is temporarily unavailable. |
|
|
1100
|
+
| `ErrRateLimited` | `ErroredState` | yes | Runtime work is temporarily rate limited. |
|
|
1101
|
+
| `ErrConflict` | `ErroredState` | yes | The learner intent conflicts with another in-flight or current runtime action. |
|
|
1102
|
+
| `ErrJsonParse` | `ErroredState` | yes | Runtime data could not be interpreted as the SDK contract. |
|
|
1103
|
+
| `ErrInvalidSubmission` | `ErroredState` | no | Renderer submitted a value that is invalid for the active interaction. |
|
|
1104
|
+
| `ErrBadRequest` | `FatalState` | no | Runtime request violates the SDK contract. |
|
|
1105
|
+
| `ErrInvalidAccessToken` | `FatalState` | no | Learner token is invalid. |
|
|
1106
|
+
| `ErrTokenExpired` | `FatalState` | no | Learner token expired. |
|
|
1107
|
+
| `ErrForbidden` | `FatalState` | no | Learner cannot continue in this runtime scope. |
|
|
1108
|
+
| `ErrNotFound` | `FatalState` | no | Runtime scope or state is unavailable. |
|
|
1109
|
+
| `ErrSdkUpgradeRequired` | `FatalState` | no | Installed SDK version is too old for Primer. |
|
|
1110
|
+
| `ErrUnsupportedPci` | `FatalState` | no | Renderer did not declare support for the presented PCI. |
|
|
1111
|
+
| `ErrNotSerializable` | thrown by `toJSON` | no | A live `PrimerState` was serialized. |
|
|
1112
|
+
|
|
1113
|
+
## Error-Handling Recipes
|
|
1114
|
+
|
|
1115
|
+
Handle create-time errors before using state:
|
|
1116
|
+
|
|
1117
|
+
```ts
|
|
1118
|
+
const result = await errors.try(create(options))
|
|
314
1119
|
if (result.error) {
|
|
315
|
-
if (errors.is(result.error,
|
|
316
|
-
|
|
1120
|
+
if (errors.is(result.error, ErrAuthCancelled)) {
|
|
1121
|
+
renderTryAgain()
|
|
1122
|
+
return
|
|
1123
|
+
}
|
|
1124
|
+
logger.error("primer create failed", { error: result.error })
|
|
1125
|
+
throw result.error
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
let state = result.data
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
Handle runtime errors through the state machine:
|
|
1132
|
+
|
|
1133
|
+
```ts
|
|
1134
|
+
if (state.phase === "errored") {
|
|
1135
|
+
if (state.retriable) {
|
|
1136
|
+
state = await state.retry()
|
|
317
1137
|
return
|
|
318
1138
|
}
|
|
319
|
-
|
|
1139
|
+
logger.error("primer non-retriable state", { error: state.error })
|
|
1140
|
+
throw state.error
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (state.phase === "fatal") {
|
|
1144
|
+
if (errors.is(state.error, ErrTokenExpired)) {
|
|
320
1145
|
renderSignInAgain()
|
|
321
1146
|
return
|
|
322
1147
|
}
|
|
323
|
-
|
|
1148
|
+
if (errors.is(state.error, ErrSdkUpgradeRequired)) {
|
|
1149
|
+
renderSdkUpgradeMessage()
|
|
1150
|
+
return
|
|
1151
|
+
}
|
|
1152
|
+
logger.error("primer fatal state", { error: state.error })
|
|
1153
|
+
throw state.error
|
|
324
1154
|
}
|
|
325
1155
|
```
|
|
326
1156
|
|
|
327
|
-
|
|
1157
|
+
Handle invalid submissions by fixing renderer state, not by retrying the same invalid intent:
|
|
328
1158
|
|
|
329
|
-
|
|
1159
|
+
```ts
|
|
1160
|
+
const next = await state.submitChoice(selectedKeys)
|
|
1161
|
+
if (next.phase === "errored") {
|
|
1162
|
+
if (errors.is(next.error, ErrInvalidSubmission)) {
|
|
1163
|
+
renderSelectionError(next.error.message)
|
|
1164
|
+
return
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
state = next
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
## Logger
|
|
330
1171
|
|
|
331
1172
|
```ts
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
)
|
|
1173
|
+
import type { PrimerLogger } from "@superbuilders/primer-tives/logger"
|
|
1174
|
+
|
|
1175
|
+
interface PrimerLogger {
|
|
1176
|
+
debug(message: string, attributes?: Record<string, unknown>): void
|
|
1177
|
+
info(message: string, attributes?: Record<string, unknown>): void
|
|
1178
|
+
warn(message: string, attributes?: Record<string, unknown>): void
|
|
1179
|
+
error(message: string, attributes?: Record<string, unknown>): void
|
|
340
1180
|
}
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
`@superbuilders/slog` matches this interface directly.
|
|
1184
|
+
|
|
1185
|
+
```ts
|
|
1186
|
+
import * as logger from "@superbuilders/slog"
|
|
1187
|
+
|
|
1188
|
+
const state = await create({
|
|
1189
|
+
origin,
|
|
1190
|
+
publishableKey,
|
|
1191
|
+
subject: "vocabulary",
|
|
1192
|
+
logger
|
|
1193
|
+
})
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
## Grade Levels
|
|
1197
|
+
|
|
1198
|
+
```ts
|
|
1199
|
+
import { GRADE_LEVELS } from "@superbuilders/primer-tives/grade-level"
|
|
1200
|
+
import type { GradeLevel } from "@superbuilders/primer-tives/grade-level"
|
|
1201
|
+
|
|
1202
|
+
const gradeLevels = GRADE_LEVELS
|
|
1203
|
+
type RuntimeGradeLevel = GradeLevel
|
|
1204
|
+
```
|
|
1205
|
+
|
|
1206
|
+
```ts
|
|
1207
|
+
const GRADE_LEVELS = ["K", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"] as const
|
|
1208
|
+
type GradeLevel = (typeof GRADE_LEVELS)[number]
|
|
1209
|
+
```
|
|
1210
|
+
|
|
1211
|
+
Grade level is not a `create` option. Treat grade-level identifiers as content/runtime data when Primer presents them, not as an SDK lifecycle input.
|
|
1212
|
+
|
|
1213
|
+
## Testing
|
|
1214
|
+
|
|
1215
|
+
`PrimerOptions.fetch` exists so tests, host runtimes, and instrumentation can provide a fetch-compatible function. The SDK treats it exactly as the runtime communication function for `create`, transitions, submissions, retries, and timeouts.
|
|
1216
|
+
|
|
1217
|
+
The runtime exchange shape is not public SDK surface. Tests should assert SDK semantics after `create` resolves or rejects:
|
|
1218
|
+
|
|
1219
|
+
| Scenario | Assert |
|
|
1220
|
+
| --- | --- |
|
|
1221
|
+
| auth fails | `create` rejects with the expected auth sentinel |
|
|
1222
|
+
| first runtime work fails recoverably | `create` resolves to `ErroredState` with `retriable: true` |
|
|
1223
|
+
| first runtime work fails terminally | `create` resolves to `FatalState` |
|
|
1224
|
+
| unsupported PCI is presented | `create` resolves to `FatalState` with `ErrUnsupportedPci` |
|
|
1225
|
+
| standard submission is invalid | submit method resolves to `ErroredState` with `ErrInvalidSubmission` |
|
|
1226
|
+
| concurrent submit/timeout conflict occurs | transition resolves to `ErroredState` with `ErrConflict` |
|
|
1227
|
+
| state is serialized | serialization throws `ErrNotSerializable` |
|
|
1228
|
+
|
|
1229
|
+
Example test shape:
|
|
1230
|
+
|
|
1231
|
+
```ts
|
|
1232
|
+
import * as errors from "@superbuilders/errors"
|
|
1233
|
+
import { create } from "@superbuilders/primer-tives/client"
|
|
1234
|
+
import { ErrUnsupportedPci } from "@superbuilders/primer-tives/errors"
|
|
1235
|
+
|
|
1236
|
+
declare const fetchMock: typeof globalThis.fetch
|
|
341
1237
|
|
|
342
1238
|
const state = await create({
|
|
343
1239
|
origin: "https://primer.test",
|
|
344
1240
|
publishableKey: "pk_test",
|
|
345
1241
|
accessToken: "eyJ.test.token",
|
|
346
|
-
subject: "
|
|
1242
|
+
subject: "math",
|
|
347
1243
|
fetch: fetchMock,
|
|
348
1244
|
logger
|
|
349
1245
|
})
|
|
1246
|
+
|
|
1247
|
+
if (state.phase === "fatal") {
|
|
1248
|
+
if (errors.is(state.error, ErrUnsupportedPci)) {
|
|
1249
|
+
renderRendererCapabilityError()
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
350
1252
|
```
|
|
351
1253
|
|
|
352
1254
|
## Security Model
|
|
@@ -355,35 +1257,75 @@ The browser may hold:
|
|
|
355
1257
|
|
|
356
1258
|
```txt
|
|
357
1259
|
publishable key
|
|
358
|
-
|
|
1260
|
+
learner access token
|
|
359
1261
|
```
|
|
360
1262
|
|
|
361
|
-
The publishable key identifies the Primer frontend. It
|
|
1263
|
+
The publishable key identifies the Primer frontend. It is not learner auth.
|
|
1264
|
+
|
|
1265
|
+
The access token authenticates the learner. Primer verifies it before producing learning state.
|
|
362
1266
|
|
|
363
|
-
|
|
1267
|
+
PCI support is a renderer capability declaration. It is not negotiated implicitly. If a renderer cannot handle a required PCI, it must not claim that PCI in `supportedPcis`.
|
|
364
1268
|
|
|
365
|
-
Primer does not need:
|
|
1269
|
+
Primer does not need these as public SDK inputs:
|
|
366
1270
|
|
|
367
1271
|
```txt
|
|
368
1272
|
learner email
|
|
369
1273
|
verified email
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
1274
|
+
frontend secret key
|
|
1275
|
+
student id
|
|
1276
|
+
grade level
|
|
373
1277
|
```
|
|
374
1278
|
|
|
1279
|
+
## Integration Checklist
|
|
1280
|
+
|
|
1281
|
+
1. Import `create` from `@superbuilders/primer-tives/client`.
|
|
1282
|
+
2. Import shared renderer contracts from `@superbuilders/primer-tives/contracts`.
|
|
1283
|
+
3. Pass `origin`, `publishableKey`, and `logger` to `create`.
|
|
1284
|
+
4. Either pass `accessToken` or let `create` perform SDK-managed auth.
|
|
1285
|
+
5. Choose a public `subject`, or omit `subject` for all-subject runtime scope.
|
|
1286
|
+
6. Declare every required renderer PCI in `supportedPcis`.
|
|
1287
|
+
7. Await `create` and handle create-time errors with `errors.try`.
|
|
1288
|
+
8. Render by switching on `state.phase`.
|
|
1289
|
+
9. For interaction states, render by switching on `state.kind`.
|
|
1290
|
+
10. Use only the transition methods exposed by the current state.
|
|
1291
|
+
11. Handle `ErroredState` through `retriable` and `retry()`.
|
|
1292
|
+
12. Handle `FatalState` as terminal for the current state object.
|
|
1293
|
+
13. Never serialize `PrimerState`.
|
|
1294
|
+
14. Recreate state with `create` after reload, remount, account switch, or subject switch.
|
|
1295
|
+
|
|
1296
|
+
## What This SDK Does Not Expose
|
|
1297
|
+
|
|
1298
|
+
The current SDK intentionally does not expose:
|
|
1299
|
+
|
|
1300
|
+
| Not exposed | Use instead |
|
|
1301
|
+
| --- | --- |
|
|
1302
|
+
| package-root exports | explicit public subpaths |
|
|
1303
|
+
| backend-only SDK surface | browser/client semantic SDK only |
|
|
1304
|
+
| public auth result union | `create` |
|
|
1305
|
+
| separate auth API | `create` |
|
|
1306
|
+
| client wrapper object | `create` returning `Promise<PrimerState>` |
|
|
1307
|
+
| `start()` | `create` already starts the first state |
|
|
1308
|
+
| `snapshot()` | live `PrimerState` only |
|
|
1309
|
+
| serializable state | recreate with `create` |
|
|
1310
|
+
| public `"all"` subject value | omit `subject` |
|
|
1311
|
+
| implicit PCI negotiation | explicit `supportedPcis` |
|
|
1312
|
+
| grade-level lifecycle option | runtime content/state handling |
|
|
1313
|
+
|
|
375
1314
|
## Final Invariants
|
|
376
1315
|
|
|
377
1316
|
```txt
|
|
378
1317
|
create is the only public lifecycle operation
|
|
379
1318
|
create returns Promise<PrimerState>
|
|
380
|
-
accessToken present skips
|
|
381
|
-
accessToken absent uses
|
|
382
|
-
subject is optional; omitted means
|
|
1319
|
+
accessToken present skips SDK-managed auth
|
|
1320
|
+
accessToken absent uses SDK-managed auth
|
|
1321
|
+
subject is optional; omitted means all-subject runtime scope
|
|
383
1322
|
subject determines required renderer PCI capabilities
|
|
384
1323
|
supportedPcis declares renderer PCI capabilities
|
|
385
|
-
PrimerState is the learning state machine
|
|
1324
|
+
PrimerState is the live learning state machine
|
|
386
1325
|
only valid state variants expose learning transitions
|
|
1326
|
+
standard interaction submissions are validated before runtime submission
|
|
1327
|
+
fatal state is terminal for the current state object
|
|
1328
|
+
live state is not serializable
|
|
387
1329
|
```
|
|
388
1330
|
|
|
389
|
-
Keep these concepts separate
|
|
1331
|
+
Keep these concepts separate: publishable key is not learner auth; access token is not content authorization; subject selects scope but does not prove renderer capability; PCI support is explicit; `PrimerState` is live behavior, not data.
|