@a4anthony/proctorkit-types 0.3.0 → 0.4.0

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.
@@ -0,0 +1,548 @@
1
+ import type { EventKind } from "../events.js";
2
+
3
+ export type SupportEventDisposition = "state" | "aggregate" | "latest";
4
+ export type SupportEventSection = "preflight" | "resume" | "runtime" | "writing";
5
+ export type SupportEventDataClass =
6
+ | "support_safe"
7
+ | "candidate_personal"
8
+ | "device_identifier"
9
+ | "biometric"
10
+ | "assessment_content"
11
+ | "integrity_signal"
12
+ | "internal_diagnostic"
13
+ | "secret";
14
+
15
+ /** DTO paths that the first support-context projection is allowed to derive. */
16
+ export const SUPPORT_DERIVED_FACTS = [
17
+ "lifecycle.activityState",
18
+ "lifecycle.endedAt",
19
+ "lifecycle.journeyStage",
20
+ "lifecycle.lastActivityAt",
21
+ "lifecycle.observationState",
22
+ "preflight.checks[]",
23
+ "preflight.checks[].microphoneVerification",
24
+ "preflight.completedAt",
25
+ "preflight.devices",
26
+ "preflight.devices.camera.availableCount",
27
+ "preflight.devices.camera.virtualCount",
28
+ "preflight.devices.microphone.availableCount",
29
+ "preflight.devices.microphone.virtualCount",
30
+ "preflight.devices.speaker.availableCount",
31
+ "preflight.devices.speaker.virtualCount",
32
+ "preflight.observationState",
33
+ "preflight.startedAt",
34
+ "preflight.verdict",
35
+ "resume.currentOrLatest",
36
+ "resume.totalResumes",
37
+ "runtime.activeBlocker",
38
+ "runtime.delivery.drainTimeouts",
39
+ "runtime.delivery.lastFailureCode",
40
+ "runtime.delivery.mediaFailures",
41
+ "runtime.delivery.sdkErrors",
42
+ "runtime.delivery.uploadFailures",
43
+ "runtime.delivery.workerFailures",
44
+ "runtime.issues[]",
45
+ "runtime.observationState",
46
+ "writing.clipboard.blockedCopyAttempts",
47
+ "writing.clipboard.blockedCutAttempts",
48
+ "writing.clipboard.blockedPasteAttempts",
49
+ "writing.clipboard.lastBlockedAction",
50
+ "writing.clipboard.lastBlockedAt",
51
+ "writing.lastSaveFailure",
52
+ "writing.saveFailures",
53
+ ] as const;
54
+
55
+ export type SupportDerivedFact = (typeof SUPPORT_DERIVED_FACTS)[number];
56
+
57
+ export interface SupportEventPayloadFieldDefinition {
58
+ dataClasses: SupportEventDataClass[];
59
+ description: string;
60
+ }
61
+
62
+ export interface IncludedSupportEventDecision {
63
+ disposition: SupportEventDisposition;
64
+ section: SupportEventSection;
65
+ allowedPayloadFields: string[];
66
+ derivedFacts: SupportDerivedFact[];
67
+ resolvedBy?: EventKind[];
68
+ resolves?: EventKind[];
69
+ absenceMeans: string;
70
+ }
71
+
72
+ export interface ExcludedSupportEventDecision {
73
+ disposition: "exclude";
74
+ reason: string;
75
+ }
76
+
77
+ export interface ExcludedSupportEventSource extends ExcludedSupportEventDecision {
78
+ kind: EventKind;
79
+ }
80
+
81
+ export type CandidateSupportEventDecision =
82
+ | IncludedSupportEventDecision
83
+ | ExcludedSupportEventDecision;
84
+
85
+ export interface SupportEventDefinitionSource {
86
+ decision: IncludedSupportEventDecision;
87
+ payload: Record<string, SupportEventPayloadFieldDefinition>;
88
+ }
89
+
90
+ const SAFE_OPERATIONAL_FIELDS = {
91
+ code: "A stable machine-readable support code.",
92
+ phase: "A stable SDK lifecycle phase.",
93
+ recoverable: "Whether the emitting producer classified the failure as recoverable.",
94
+ row: "A stable preflight check kind.",
95
+ durationMs: "Elapsed check time in milliseconds.",
96
+ retryCount: "Retries observed before this result.",
97
+ attempt: "The current retry ordinal.",
98
+ downloadMbps: "Measured downstream throughput in megabits per second.",
99
+ uploadMbps: "Measured upstream throughput in megabits per second.",
100
+ latencyMs: "Measured connection latency in milliseconds.",
101
+ jitterMs: "Measured connection jitter in milliseconds.",
102
+ recordingUploadMode: "The selected stable recording upload mode.",
103
+ readinessMedianMs: "Median readiness request duration in milliseconds.",
104
+ readinessWindowMs: "Readiness sampling window in milliseconds.",
105
+ readinessSampleCount: "Number of readiness samples.",
106
+ verificationMode: "The stable microphone verification mode.",
107
+ fallbackReason: "The stable microphone verification fallback code.",
108
+ vadPreparationMs: "Voice-activity detector preparation time in milliseconds.",
109
+ class: "The stable resume interruption class.",
110
+ awayMs: "Observed unavailability duration in milliseconds.",
111
+ navigationType: "The browser navigation category.",
112
+ surface: "The browser display-surface category.",
113
+ passed: "Whether the completed preflight attempt passed.",
114
+ blocked: "Whether policy blocked the clipboard action.",
115
+ } as const;
116
+
117
+ export const SUPPORT_SAFE_PAYLOAD_FIELDS = Object.fromEntries(
118
+ Object.entries(SAFE_OPERATIONAL_FIELDS).map(([name, description]) => [
119
+ name,
120
+ { dataClasses: ["support_safe"], description },
121
+ ]),
122
+ ) as Record<keyof typeof SAFE_OPERATIONAL_FIELDS, SupportEventPayloadFieldDefinition>;
123
+
124
+ const decision = (
125
+ disposition: SupportEventDisposition,
126
+ section: SupportEventSection,
127
+ derivedFacts: SupportDerivedFact[],
128
+ allowedPayloadFields: Array<keyof typeof SAFE_OPERATIONAL_FIELDS> = [],
129
+ relationships: Pick<IncludedSupportEventDecision, "resolvedBy" | "resolves"> = {},
130
+ ): SupportEventDefinitionSource => ({
131
+ decision: {
132
+ disposition,
133
+ section,
134
+ allowedPayloadFields,
135
+ derivedFacts,
136
+ ...relationships,
137
+ absenceMeans:
138
+ "Absence is inconclusive unless the corresponding section reports complete collection.",
139
+ },
140
+ payload: Object.fromEntries(
141
+ allowedPayloadFields.map((field) => [field, SUPPORT_SAFE_PAYLOAD_FIELDS[field]]),
142
+ ),
143
+ });
144
+
145
+ /**
146
+ * Candidate-safe operational events only. Payload fields are deliberately
147
+ * allowlisted; the server must still map them into normalized facts and must
148
+ * never use this metadata to serialize an event payload automatically.
149
+ */
150
+ export const INCLUDED_SUPPORT_EVENT_DEFINITIONS = {
151
+ "session.started": decision("state", "runtime", ["lifecycle.journeyStage"]),
152
+ "session.heartbeat": decision("latest", "runtime", ["lifecycle.lastActivityAt"]),
153
+ "session.end_requested": decision("state", "runtime", ["lifecycle.activityState"]),
154
+ "session.ended": decision("latest", "runtime", ["lifecycle.endedAt"]),
155
+ "session.resumed": decision(
156
+ "aggregate",
157
+ "resume",
158
+ ["resume.totalResumes", "resume.currentOrLatest"],
159
+ ["class", "awayMs", "navigationType"],
160
+ ),
161
+ "session.abandoned": decision("latest", "runtime", ["lifecycle.activityState"]),
162
+ "sdk.initialized": decision("state", "runtime", ["lifecycle.observationState"]),
163
+ "sdk.delivery.ready": decision("state", "runtime", ["runtime.observationState"]),
164
+ "sdk.ready": decision("state", "runtime", ["lifecycle.journeyStage"]),
165
+ "sdk.error": decision(
166
+ "aggregate",
167
+ "runtime",
168
+ ["runtime.delivery.sdkErrors", "runtime.delivery.lastFailureCode"],
169
+ ["code", "phase", "recoverable"],
170
+ ),
171
+ "sdk.upload.failed": decision(
172
+ "aggregate",
173
+ "runtime",
174
+ ["runtime.delivery.uploadFailures", "runtime.delivery.lastFailureCode"],
175
+ ["code", "phase", "recoverable"],
176
+ ),
177
+ "sdk.media.failed": decision(
178
+ "aggregate",
179
+ "runtime",
180
+ ["runtime.delivery.mediaFailures", "runtime.delivery.lastFailureCode"],
181
+ ["code", "phase", "recoverable"],
182
+ ),
183
+ "sdk.permission.denied": decision(
184
+ "aggregate",
185
+ "runtime",
186
+ ["runtime.issues[]"],
187
+ ["code", "phase", "recoverable"],
188
+ ),
189
+ "sdk.worker.failed": decision(
190
+ "aggregate",
191
+ "runtime",
192
+ ["runtime.delivery.workerFailures", "runtime.delivery.lastFailureCode"],
193
+ ["code", "phase", "recoverable"],
194
+ ),
195
+ "sdk.storage.fallback": decision("latest", "runtime", ["runtime.issues[]"]),
196
+ "sdk.recording.buffer-degraded": decision("aggregate", "runtime", ["runtime.issues[]"]),
197
+ "sdk.recording.buffer-evicted": decision("aggregate", "runtime", ["runtime.issues[]"]),
198
+ "sdk.stop.drain-timeout": decision(
199
+ "aggregate",
200
+ "runtime",
201
+ ["runtime.delivery.drainTimeouts"],
202
+ ["code"],
203
+ ),
204
+ "screen-share.started": decision("state", "runtime", ["runtime.issues[]"], [], {
205
+ resolves: ["screen-share.declined", "screen-share.wrong-surface"],
206
+ }),
207
+ "screen-share.stopped": decision("latest", "runtime", ["runtime.issues[]"]),
208
+ "screen-share.declined": decision("aggregate", "runtime", ["runtime.issues[]"], [], {
209
+ resolvedBy: ["screen-share.started"],
210
+ }),
211
+ "screen-share.wrong-surface": decision(
212
+ "aggregate",
213
+ "runtime",
214
+ ["runtime.issues[]"],
215
+ ["surface"],
216
+ { resolvedBy: ["screen-share.started"] },
217
+ ),
218
+ "screen-share.recording.started": decision("state", "runtime", ["runtime.issues[]"]),
219
+ "screen-share.recording.stopped": decision("latest", "runtime", ["runtime.issues[]"]),
220
+ "screen-share.recording.unavailable": decision("aggregate", "runtime", ["runtime.issues[]"]),
221
+ "webcam.started": decision("state", "runtime", ["runtime.issues[]"], [], {
222
+ resolves: ["webcam.declined", "webcam.unavailable"],
223
+ }),
224
+ "webcam.stopped": decision("latest", "runtime", ["runtime.issues[]"]),
225
+ "webcam.declined": decision("aggregate", "runtime", ["runtime.issues[]"], [], {
226
+ resolvedBy: ["webcam.started"],
227
+ }),
228
+ "webcam.unavailable": decision("aggregate", "runtime", ["runtime.issues[]"], [], {
229
+ resolvedBy: ["webcam.started"],
230
+ }),
231
+ "audio-playback.started": decision("state", "preflight", ["preflight.checks[]"]),
232
+ "audio-playback.paused": decision("latest", "preflight", ["preflight.checks[]"]),
233
+ "audio-playback.stopped": decision("latest", "preflight", ["preflight.checks[]"]),
234
+ "audio-playback.ended": decision("latest", "preflight", ["preflight.checks[]"]),
235
+ "audio-playback.error": decision("aggregate", "preflight", [
236
+ "preflight.checks[]",
237
+ "runtime.issues[]",
238
+ ]),
239
+ "audio-playback.sink-applied": decision("latest", "preflight", ["preflight.checks[]"]),
240
+ "audio-playback.sink-unsupported": decision("latest", "preflight", ["preflight.checks[]"]),
241
+ "audio-playback.sink-failed": decision("aggregate", "preflight", [
242
+ "preflight.checks[]",
243
+ "runtime.issues[]",
244
+ ]),
245
+ "audio-playback.sink-disconnected": decision("aggregate", "preflight", [
246
+ "preflight.checks[]",
247
+ "runtime.issues[]",
248
+ ]),
249
+ "video-playback.started": decision("state", "runtime", ["runtime.issues[]"]),
250
+ "video-playback.paused": decision("latest", "runtime", ["runtime.issues[]"]),
251
+ "video-playback.stopped": decision("latest", "runtime", ["runtime.issues[]"]),
252
+ "video-playback.ended": decision("latest", "runtime", ["runtime.issues[]"]),
253
+ "video-playback.error": decision("aggregate", "runtime", ["runtime.issues[]"]),
254
+ "video-playback.sink-applied": decision("latest", "runtime", ["runtime.issues[]"]),
255
+ "video-playback.sink-unsupported": decision("latest", "runtime", ["runtime.issues[]"]),
256
+ "video-playback.sink-failed": decision("aggregate", "runtime", ["runtime.issues[]"]),
257
+ "video-playback.sink-disconnected": decision("aggregate", "runtime", ["runtime.issues[]"]),
258
+ "network.online": decision("state", "runtime", ["runtime.issues[]"], [], {
259
+ resolves: ["network.offline"],
260
+ }),
261
+ "network.offline": decision("state", "runtime", ["runtime.issues[]"], [], {
262
+ resolvedBy: ["network.online"],
263
+ }),
264
+ "preflight.started": decision(
265
+ "state",
266
+ "preflight",
267
+ ["preflight.observationState", "preflight.startedAt"],
268
+ [],
269
+ { resolvedBy: ["preflight.completed"] },
270
+ ),
271
+ "preflight.row-checking": decision("latest", "preflight", ["preflight.checks[]"], ["row"], {
272
+ resolvedBy: ["preflight.row-passed", "preflight.row-failed"],
273
+ }),
274
+ "preflight.row-passed": decision(
275
+ "latest",
276
+ "preflight",
277
+ ["preflight.checks[]"],
278
+ [
279
+ "row",
280
+ "durationMs",
281
+ "downloadMbps",
282
+ "uploadMbps",
283
+ "latencyMs",
284
+ "jitterMs",
285
+ "recordingUploadMode",
286
+ "readinessMedianMs",
287
+ "readinessWindowMs",
288
+ "readinessSampleCount",
289
+ ],
290
+ { resolves: ["preflight.row-checking", "preflight.row-failed"] },
291
+ ),
292
+ "preflight.row-failed": decision(
293
+ "aggregate",
294
+ "preflight",
295
+ ["preflight.checks[]", "runtime.issues[]"],
296
+ [
297
+ "row",
298
+ "code",
299
+ "durationMs",
300
+ "retryCount",
301
+ "downloadMbps",
302
+ "uploadMbps",
303
+ "latencyMs",
304
+ "jitterMs",
305
+ "recordingUploadMode",
306
+ "readinessMedianMs",
307
+ "readinessWindowMs",
308
+ "readinessSampleCount",
309
+ ],
310
+ {
311
+ resolvedBy: ["preflight.row-retried", "preflight.row-passed"],
312
+ resolves: ["preflight.row-checking"],
313
+ },
314
+ ),
315
+ "preflight.row-retried": decision(
316
+ "aggregate",
317
+ "preflight",
318
+ ["preflight.checks[]"],
319
+ ["row", "attempt"],
320
+ { resolves: ["preflight.row-failed"] },
321
+ ),
322
+ "preflight.device-picked": decision("latest", "preflight", ["preflight.devices"]),
323
+ "preflight.devices-enumerated": decision("latest", "preflight", [
324
+ "preflight.devices.camera.availableCount",
325
+ "preflight.devices.camera.virtualCount",
326
+ "preflight.devices.microphone.availableCount",
327
+ "preflight.devices.microphone.virtualCount",
328
+ "preflight.devices.speaker.availableCount",
329
+ "preflight.devices.speaker.virtualCount",
330
+ ]),
331
+ "preflight.devices-changed": decision("aggregate", "preflight", [
332
+ "preflight.devices.camera.availableCount",
333
+ "preflight.devices.camera.virtualCount",
334
+ "preflight.devices.microphone.availableCount",
335
+ "preflight.devices.microphone.virtualCount",
336
+ "preflight.devices.speaker.availableCount",
337
+ "preflight.devices.speaker.virtualCount",
338
+ ]),
339
+ "preflight.microphone-verification": decision(
340
+ "latest",
341
+ "preflight",
342
+ ["preflight.checks[].microphoneVerification"],
343
+ ["verificationMode", "fallbackReason", "vadPreparationMs"],
344
+ ),
345
+ "preflight.face-detected": decision("latest", "preflight", ["preflight.checks[]"]),
346
+ "preflight.completed": decision(
347
+ "latest",
348
+ "preflight",
349
+ ["preflight.verdict", "preflight.completedAt"],
350
+ ["passed"],
351
+ { resolves: ["preflight.started"] },
352
+ ),
353
+ "clipboard.copy": decision(
354
+ "aggregate",
355
+ "writing",
356
+ [
357
+ "writing.clipboard.blockedCopyAttempts",
358
+ "writing.clipboard.lastBlockedAction",
359
+ "writing.clipboard.lastBlockedAt",
360
+ ],
361
+ ["blocked"],
362
+ ),
363
+ "clipboard.paste": decision(
364
+ "aggregate",
365
+ "writing",
366
+ [
367
+ "writing.clipboard.blockedPasteAttempts",
368
+ "writing.clipboard.lastBlockedAction",
369
+ "writing.clipboard.lastBlockedAt",
370
+ ],
371
+ ["blocked"],
372
+ ),
373
+ "clipboard.cut": decision(
374
+ "aggregate",
375
+ "writing",
376
+ [
377
+ "writing.clipboard.blockedCutAttempts",
378
+ "writing.clipboard.lastBlockedAction",
379
+ "writing.clipboard.lastBlockedAt",
380
+ ],
381
+ ["blocked"],
382
+ ),
383
+ "text.saved": decision("latest", "writing", ["writing.lastSaveFailure"], [], {
384
+ resolves: ["text.save-failed"],
385
+ }),
386
+ "text.save-failed": decision(
387
+ "aggregate",
388
+ "writing",
389
+ ["writing.saveFailures", "writing.lastSaveFailure"],
390
+ ["code"],
391
+ { resolvedBy: ["text.saved"] },
392
+ ),
393
+ } satisfies Partial<Record<EventKind, SupportEventDefinitionSource>>;
394
+
395
+ const INTEGRITY_AND_BEHAVIOUR_EVENTS = [
396
+ "session.integrity-hold",
397
+ "session.integrity-cleared",
398
+ "session.checkpoint",
399
+ "focus.lost",
400
+ "focus.gained",
401
+ "tab.hidden",
402
+ "tab.visible",
403
+ "page.viewed",
404
+ "fullscreen.entered",
405
+ "fullscreen.exited",
406
+ "contextmenu.opened",
407
+ "keyboard.blocked",
408
+ "screenshot.attempted",
409
+ "pointer.left-window",
410
+ "pointer.returned",
411
+ "idle.started",
412
+ "idle.ended",
413
+ ] as const satisfies readonly EventKind[];
414
+
415
+ const MEDIA_AND_RECORDING_EVENTS = [
416
+ "screen-share.chunk-uploaded",
417
+ "screen-share.chunk-dropped",
418
+ "assessment-replay.started",
419
+ "assessment-replay.chunk-uploaded",
420
+ "assessment-replay.chunk-dropped",
421
+ "assessment-replay.unavailable",
422
+ "assessment-replay.stopped",
423
+ "webcam.photo.captured",
424
+ "webcam.photo.dropped",
425
+ "webcam.recording.started",
426
+ "webcam.recording.stopped",
427
+ "webcam.recording.chunk-uploaded",
428
+ "webcam.recording.chunk-dropped",
429
+ "video-clip.started",
430
+ "video-clip.stopped",
431
+ "video-clip.uploaded",
432
+ "video-clip.dropped",
433
+ "audio-clip.started",
434
+ "audio-clip.stopped",
435
+ "audio-clip.uploaded",
436
+ "audio-clip.dropped",
437
+ "media.frame.captured",
438
+ "media.audio.chunk",
439
+ ] as const satisfies readonly EventKind[];
440
+
441
+ const DEVICE_AND_POLICY_EVENTS = [
442
+ "session.fingerprint",
443
+ "session.policy",
444
+ "preflight.camera-photo",
445
+ ] as const satisfies readonly EventKind[];
446
+
447
+ const UNTRUSTED_OPEN_PAYLOAD_EVENTS = [
448
+ "preflight.attestation",
449
+ ] as const satisfies readonly EventKind[];
450
+
451
+ const ANALYSIS_AND_SURVEILLANCE_EVENTS = [
452
+ "face.detector-ready",
453
+ "face.detector-failed",
454
+ "face.detector-degraded",
455
+ "face.lost",
456
+ "face.returned",
457
+ "face.multiple",
458
+ "face.multiple-cleared",
459
+ "face.gaze-off-screen",
460
+ "face.gaze-restored",
461
+ "face.identity-mismatch",
462
+ "screen.window-switched",
463
+ "screen.scene-stable",
464
+ "screen.recording-paused",
465
+ "webcam.phone-detected",
466
+ "webcam.phone-cleared",
467
+ "webcam.book-detected",
468
+ "webcam.book-cleared",
469
+ "webcam.second-device-detected",
470
+ "webcam.second-device-cleared",
471
+ "webcam.second-screen-detected",
472
+ "webcam.second-screen-cleared",
473
+ "face.lost-at-snapshot",
474
+ "face.multiple-at-snapshot",
475
+ "face.gaze-off-screen-at-snapshot",
476
+ "face.identity-mismatch-at-snapshot",
477
+ "webcam.phone-detected-at-snapshot",
478
+ "webcam.book-detected-at-snapshot",
479
+ "webcam.second-device-detected-at-snapshot",
480
+ "webcam.second-screen-detected-at-snapshot",
481
+ ] as const satisfies readonly EventKind[];
482
+
483
+ const ASSESSMENT_WRITING_EVENTS = [
484
+ "text.started",
485
+ "text.paste",
486
+ "text.copy",
487
+ "text.cut",
488
+ "text.keystroke-summary",
489
+ "text.checkpoint",
490
+ "text.focus-lost",
491
+ "text.focus-regained",
492
+ "text.synthetic-input",
493
+ "text.submitted",
494
+ ] as const satisfies readonly EventKind[];
495
+
496
+ const exclude = (events: readonly EventKind[], reason: string): ExcludedSupportEventSource[] =>
497
+ events.map((kind) => ({ kind, disposition: "exclude", reason }));
498
+
499
+ /**
500
+ * Every excluded event is named explicitly. The generator rejects duplicates,
501
+ * omissions, and unknown events so a new EventKind cannot silently inherit a
502
+ * candidate-support policy.
503
+ */
504
+ export const EXCLUDED_SUPPORT_EVENT_SOURCES = [
505
+ ...exclude(
506
+ INTEGRITY_AND_BEHAVIOUR_EVENTS,
507
+ "Candidate-facing support excludes behavioural and integrity-surveillance telemetry.",
508
+ ),
509
+ ...exclude(
510
+ MEDIA_AND_RECORDING_EVENTS,
511
+ "Candidate-facing support excludes media, recording, upload, and evidence telemetry.",
512
+ ),
513
+ ...exclude(
514
+ DEVICE_AND_POLICY_EVENTS,
515
+ "The payload may expose raw device identifiers, fingerprints, biometric media, or internal policy.",
516
+ ),
517
+ ...exclude(
518
+ UNTRUSTED_OPEN_PAYLOAD_EVENTS,
519
+ "Candidate-facing support excludes untrusted open payloads until a closed server mapping exists.",
520
+ ),
521
+ ...exclude(
522
+ ANALYSIS_AND_SURVEILLANCE_EVENTS,
523
+ "Candidate-facing support excludes biometric, object, screen-analysis, and review evidence.",
524
+ ),
525
+ ...exclude(
526
+ ASSESSMENT_WRITING_EVENTS,
527
+ "Candidate-facing support excludes answers, questions, clipboard content, typing dynamics, and writing-integrity signals.",
528
+ ),
529
+ ] satisfies ExcludedSupportEventSource[];
530
+
531
+ const buildExcludedSupportEventDecisions = (
532
+ sources: readonly ExcludedSupportEventSource[],
533
+ ): Partial<Record<EventKind, ExcludedSupportEventDecision>> => {
534
+ const seen = new Set<EventKind>();
535
+ for (const { kind } of sources) {
536
+ if (seen.has(kind)) {
537
+ throw new Error(`Duplicate candidate-support exclusion source: ${kind}`);
538
+ }
539
+ seen.add(kind);
540
+ }
541
+ return Object.fromEntries(
542
+ sources.map(({ kind, disposition, reason }) => [kind, { disposition, reason }]),
543
+ ) as Partial<Record<EventKind, ExcludedSupportEventDecision>>;
544
+ };
545
+
546
+ export const EXCLUDED_SUPPORT_EVENT_DECISIONS = buildExcludedSupportEventDecisions(
547
+ EXCLUDED_SUPPORT_EVENT_SOURCES,
548
+ );
@@ -0,0 +1,3 @@
1
+ export const PROCTORKIT_TELEMETRY_SCHEMA_VERSION = "1.0.0";
2
+ export const PROCTORKIT_SESSION_CONTEXT_FORMAT_VERSION = "1.0.0";
3
+ export const PROCTORKIT_SUPPORT_PROJECTION_VERSION = "1.0.0";