@emilia-protocol/gate 0.18.1 → 0.18.2

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/src/referee.ts ADDED
@@ -0,0 +1,625 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Deterministic, offline EMILIA Referee core.
4
+ *
5
+ * The referee does not discover trust roots, executables, protocols, policy,
6
+ * or network state. It evaluates one caller-pinned runner output as a
7
+ * non-authorizing self-test claim and keeps native verification, relying-party
8
+ * acceptance, binding, evidence composition, provider outcome, and observed
9
+ * effect as independent facts.
10
+ */
11
+
12
+ import path from 'node:path';
13
+
14
+ export const REFEREE_EVALUATION_VERSION = 'EP-REFEREE-EVALUATION-v1';
15
+ export const REFEREE_RUNNER_REQUEST_VERSION = 'EP-REFEREE-RUNNER-REQUEST-v1';
16
+ export const REFEREE_RUNNER_OUTPUT_VERSION = 'EP-REFEREE-RUNNER-OUTPUT-v1';
17
+ export const REFEREE_RESULT_VERSION = 'EP-REFEREE-RESULT-v1';
18
+
19
+ // Explicit aliases make the wire-version names easy to discover without
20
+ // changing the single canonical values used by the schemas.
21
+ export const EP_REFEREE_EVALUATION_VERSION = REFEREE_EVALUATION_VERSION;
22
+ export const EP_REFEREE_RUNNER_REQUEST_VERSION = REFEREE_RUNNER_REQUEST_VERSION;
23
+ export const EP_REFEREE_RUNNER_OUTPUT_VERSION = REFEREE_RUNNER_OUTPUT_VERSION;
24
+ export const EP_REFEREE_RESULT_VERSION = REFEREE_RESULT_VERSION;
25
+
26
+ const MAX_IDENTIFIER_BYTES = 512;
27
+ const MAX_EXECUTABLE_BYTES = 4 * 1024;
28
+ const MAX_RUNNER_ARGUMENTS = 256;
29
+ const MAX_RUNNER_ARGUMENT_BYTES = 64 * 1024;
30
+ const MAX_RUNNER_ARGUMENT_VECTOR_BYTES = 1024 * 1024;
31
+ const MAX_JSON_DEPTH = 64;
32
+
33
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,511}$/;
34
+ const CAID = /^caid:1:[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*\.[1-9][0-9]*:jcs-sha256:[A-Za-z0-9_-]{43}$/;
35
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
36
+ const REASON_CODE = /^[a-z][a-z0-9_]{0,127}$/;
37
+
38
+ const EVALUATION_KEYS = Object.freeze([
39
+ 'version', 'runner_pin', 'request', 'output',
40
+ ] as const);
41
+ const RUNNER_PIN_KEYS = Object.freeze([
42
+ 'executable', 'executable_sha256', 'args',
43
+ ] as const);
44
+ const RUNNER_REQUEST_KEYS = Object.freeze([
45
+ 'version', 'case_id', 'protocol_id', 'expected_caid',
46
+ 'expected_action_digest', 'aec_required', 'execution_scope', 'input',
47
+ ] as const);
48
+ const RUNNER_OUTPUT_KEYS = Object.freeze([
49
+ 'version', 'case_id', 'protocol_id', 'native_verification', 'rp_acceptance',
50
+ 'caid', 'action_digest', 'aec_satisfaction', 'provider_outcome',
51
+ 'effect_relation', 'execution_scope',
52
+ ] as const);
53
+
54
+ const NATIVE_VERIFICATIONS = new Set<RefereeNativeVerification>([
55
+ 'VERIFIED', 'REJECTED', 'INDETERMINATE',
56
+ ]);
57
+ const RP_ACCEPTANCES = new Set<RefereeRpAcceptance>([
58
+ 'ACCEPTED', 'REJECTED', 'INDETERMINATE',
59
+ ]);
60
+ const AEC_SATISFACTIONS = new Set<RefereeAecSatisfaction>([
61
+ 'SATISFIED', 'NOT_SATISFIED', 'INDETERMINATE', 'NOT_ASSESSED',
62
+ ]);
63
+ const PROVIDER_OUTCOMES = new Set<RefereeProviderOutcome>([
64
+ 'COMMITTED', 'PROVEN_NOT_COMMITTED', 'INDETERMINATE', 'NOT_ASSESSED',
65
+ ]);
66
+ const EFFECT_RELATIONS = new Set<RefereeEffectRelation>([
67
+ 'OBSERVED_AS_REQUESTED', 'DIVERGED', 'INDETERMINATE', 'NOT_ASSESSED',
68
+ ]);
69
+ const EXECUTION_SCOPES = new Set<RefereeExecutionScope>([
70
+ 'local_atomic', 'federated',
71
+ ]);
72
+
73
+ export type RefereeJson =
74
+ | null
75
+ | boolean
76
+ | number
77
+ | string
78
+ | RefereeJson[]
79
+ | { [key: string]: RefereeJson };
80
+
81
+ export type RefereeStatus =
82
+ | 'CONFORMANT'
83
+ | 'NON_CONFORMANT'
84
+ | 'INDETERMINATE';
85
+ export type RefereeExecutionScope = 'local_atomic' | 'federated';
86
+ export type RefereeNativeVerification =
87
+ | 'VERIFIED'
88
+ | 'REJECTED'
89
+ | 'INDETERMINATE';
90
+ export type RefereeRpAcceptance =
91
+ | 'ACCEPTED'
92
+ | 'REJECTED'
93
+ | 'INDETERMINATE';
94
+ export type RefereeCaidActionMatch = 'MATCH' | 'MISMATCH' | 'INDETERMINATE';
95
+ export type RefereeAecSatisfaction =
96
+ | 'SATISFIED'
97
+ | 'NOT_SATISFIED'
98
+ | 'INDETERMINATE'
99
+ | 'NOT_ASSESSED';
100
+ export type RefereeProviderOutcome =
101
+ | 'COMMITTED'
102
+ | 'PROVEN_NOT_COMMITTED'
103
+ | 'INDETERMINATE'
104
+ | 'NOT_ASSESSED';
105
+ export type RefereeEffectRelation =
106
+ | 'OBSERVED_AS_REQUESTED'
107
+ | 'DIVERGED'
108
+ | 'INDETERMINATE'
109
+ | 'NOT_ASSESSED';
110
+
111
+ export interface RefereeRunnerPinV1 {
112
+ readonly executable: string;
113
+ readonly executable_sha256: `sha256:${string}`;
114
+ readonly args: readonly string[];
115
+ }
116
+
117
+ /** The only JSON document written to a protocol runner's stdin. */
118
+ export interface RefereeRunnerRequestV1 {
119
+ readonly version: typeof REFEREE_RUNNER_REQUEST_VERSION;
120
+ readonly case_id: string;
121
+ readonly protocol_id: string;
122
+ readonly expected_caid: string;
123
+ readonly expected_action_digest: string;
124
+ readonly aec_required: boolean;
125
+ readonly execution_scope: RefereeExecutionScope;
126
+ readonly input: RefereeJson;
127
+ }
128
+
129
+ /** The only accepted JSON document on a protocol runner's stdout. */
130
+ export interface RefereeRunnerOutputV1 {
131
+ readonly version: typeof REFEREE_RUNNER_OUTPUT_VERSION;
132
+ readonly case_id: string;
133
+ readonly protocol_id: string;
134
+ readonly native_verification: RefereeNativeVerification;
135
+ readonly rp_acceptance: RefereeRpAcceptance;
136
+ readonly caid: string | null;
137
+ readonly action_digest: string | null;
138
+ readonly aec_satisfaction: RefereeAecSatisfaction;
139
+ readonly provider_outcome: RefereeProviderOutcome;
140
+ readonly effect_relation: RefereeEffectRelation;
141
+ readonly execution_scope: RefereeExecutionScope;
142
+ }
143
+
144
+ export interface RefereeEvaluationInputV1 {
145
+ readonly version: typeof REFEREE_EVALUATION_VERSION;
146
+ readonly runner_pin: RefereeRunnerPinV1;
147
+ readonly request: RefereeRunnerRequestV1;
148
+ readonly output: RefereeRunnerOutputV1;
149
+ }
150
+
151
+ export interface RefereeResultDimensionsV1 {
152
+ readonly native_verification: Readonly<{
153
+ value: RefereeNativeVerification;
154
+ }>;
155
+ readonly rp_acceptance: Readonly<{
156
+ value: RefereeRpAcceptance;
157
+ }>;
158
+ readonly caid_action_match: Readonly<{
159
+ value: RefereeCaidActionMatch;
160
+ expected_caid: string;
161
+ observed_caid: string | null;
162
+ expected_action_digest: string;
163
+ observed_action_digest: string | null;
164
+ }>;
165
+ readonly aec_satisfaction: Readonly<{
166
+ required: boolean;
167
+ value: RefereeAecSatisfaction;
168
+ }>;
169
+ readonly provider_outcome: Readonly<{
170
+ value: RefereeProviderOutcome;
171
+ }>;
172
+ readonly effect_relation: Readonly<{
173
+ value: RefereeEffectRelation;
174
+ }>;
175
+ }
176
+
177
+ /**
178
+ * A Referee result is evidence about one self-test only. Its fixed false
179
+ * execution_authorizing value is a semantic boundary, not configuration.
180
+ */
181
+ export interface RefereeResultV1 {
182
+ readonly version: typeof REFEREE_RESULT_VERSION;
183
+ readonly status: RefereeStatus;
184
+ readonly claim_scope: 'SELF_TEST';
185
+ readonly execution_authorizing: false;
186
+ readonly case_id: string;
187
+ readonly protocol_id: string;
188
+ readonly runner_pin: Readonly<RefereeRunnerPinV1>;
189
+ readonly execution_scope: RefereeExecutionScope;
190
+ readonly remote_atomicity_claimed: false;
191
+ readonly dimensions: Readonly<RefereeResultDimensionsV1>;
192
+ readonly reason_codes: readonly string[];
193
+ }
194
+
195
+ export interface RefereeIndeterminateInputV1 {
196
+ readonly runner_pin: RefereeRunnerPinV1;
197
+ readonly request: RefereeRunnerRequestV1;
198
+ readonly reason_code: string;
199
+ }
200
+
201
+ export class RefereeValidationError extends TypeError {
202
+ readonly code: string;
203
+
204
+ constructor(code: string) {
205
+ super(code);
206
+ this.name = 'RefereeValidationError';
207
+ this.code = code;
208
+ }
209
+ }
210
+
211
+ function fail(code: string): never {
212
+ throw new RefereeValidationError(code);
213
+ }
214
+
215
+ function plain(value: unknown): value is Record<string, unknown> {
216
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
217
+ return false;
218
+ }
219
+ const prototype = Object.getPrototypeOf(value);
220
+ return prototype === Object.prototype || prototype === null;
221
+ }
222
+
223
+ function exactObject<const T extends readonly string[]>(
224
+ value: unknown,
225
+ keys: T,
226
+ ): Record<T[number], unknown> {
227
+ if (!plain(value)) fail('invalid_schema');
228
+ const ownKeys = Reflect.ownKeys(value);
229
+ if (ownKeys.some((key) => typeof key !== 'string')) fail('unknown_key');
230
+ const expected = new Set<string>(keys);
231
+ for (const key of ownKeys as string[]) {
232
+ if (!expected.has(key)) fail('unknown_key');
233
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
234
+ if (!descriptor?.enumerable || !('value' in descriptor)) fail('invalid_schema');
235
+ }
236
+ for (const key of keys) {
237
+ if (!Object.prototype.hasOwnProperty.call(value, key)) fail('missing_key');
238
+ }
239
+ return value as Record<T[number], unknown>;
240
+ }
241
+
242
+ function hasUnpairedSurrogate(value: string): boolean {
243
+ for (let index = 0; index < value.length; index += 1) {
244
+ const code = value.charCodeAt(index);
245
+ if (code >= 0xd800 && code <= 0xdbff) {
246
+ const next = value.charCodeAt(index + 1);
247
+ if (next < 0xdc00 || next > 0xdfff) return true;
248
+ index += 1;
249
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
250
+ return true;
251
+ }
252
+ }
253
+ return false;
254
+ }
255
+
256
+ function assertJson(
257
+ value: unknown,
258
+ depth = 0,
259
+ ancestors = new Set<object>(),
260
+ ): asserts value is RefereeJson {
261
+ if (value === null || typeof value === 'boolean') return;
262
+ if (typeof value === 'string') {
263
+ if (hasUnpairedSurrogate(value)) fail('invalid_json');
264
+ return;
265
+ }
266
+ if (typeof value === 'number') {
267
+ if (!Number.isFinite(value)) fail('invalid_json');
268
+ return;
269
+ }
270
+ if (typeof value !== 'object') fail('invalid_json');
271
+ const containerDepth = depth + 1;
272
+ if (containerDepth > MAX_JSON_DEPTH) fail('invalid_json');
273
+ if (ancestors.has(value)) fail('invalid_json');
274
+ ancestors.add(value);
275
+ try {
276
+ if (Array.isArray(value)) {
277
+ if (Reflect.ownKeys(value).some((key) => {
278
+ if (key === 'length') return false;
279
+ return typeof key !== 'string' || !/^(?:0|[1-9][0-9]*)$/.test(key);
280
+ })) fail('invalid_json');
281
+ for (let index = 0; index < value.length; index += 1) {
282
+ const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
283
+ if (!descriptor?.enumerable || !('value' in descriptor)) fail('invalid_json');
284
+ assertJson(descriptor.value, containerDepth, ancestors);
285
+ }
286
+ return;
287
+ }
288
+ if (!plain(value)) fail('invalid_json');
289
+ for (const key of Reflect.ownKeys(value)) {
290
+ if (typeof key !== 'string' || hasUnpairedSurrogate(key)) fail('invalid_json');
291
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
292
+ if (!descriptor?.enumerable || !('value' in descriptor)) fail('invalid_json');
293
+ assertJson(descriptor.value, containerDepth, ancestors);
294
+ }
295
+ } finally {
296
+ ancestors.delete(value);
297
+ }
298
+ }
299
+
300
+ function deepFreeze<T>(value: T): Readonly<T> {
301
+ if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
302
+ for (const child of Object.values(value)) deepFreeze(child);
303
+ Object.freeze(value);
304
+ }
305
+ return value;
306
+ }
307
+
308
+ function frozenJsonCopy(value: unknown): Readonly<RefereeJson> {
309
+ assertJson(value);
310
+ return deepFreeze(structuredClone(value));
311
+ }
312
+
313
+ function identifier(value: unknown): string {
314
+ if (typeof value !== 'string'
315
+ || Buffer.byteLength(value, 'utf8') > MAX_IDENTIFIER_BYTES
316
+ || !IDENTIFIER.test(value)) fail('invalid_identifier');
317
+ return value;
318
+ }
319
+
320
+ function enumValue<T extends string>(
321
+ value: unknown,
322
+ values: ReadonlySet<T>,
323
+ code: string,
324
+ ): T {
325
+ if (typeof value !== 'string' || !values.has(value as T)) fail(code);
326
+ return value as T;
327
+ }
328
+
329
+ export function parseRefereeRunnerPin(value: unknown): Readonly<RefereeRunnerPinV1> {
330
+ const object = exactObject(value, RUNNER_PIN_KEYS);
331
+ if (typeof object.executable !== 'string'
332
+ || !path.isAbsolute(object.executable)
333
+ || object.executable.length === 0
334
+ || object.executable.includes('\0')
335
+ || hasUnpairedSurrogate(object.executable)
336
+ || Buffer.byteLength(object.executable, 'utf8') > MAX_EXECUTABLE_BYTES) {
337
+ fail('invalid_executable');
338
+ }
339
+ if (typeof object.executable_sha256 !== 'string'
340
+ || !DIGEST.test(object.executable_sha256)) {
341
+ fail('invalid_executable_digest');
342
+ }
343
+ const executableSha256 = object.executable_sha256 as `sha256:${string}`;
344
+ if (!Array.isArray(object.args) || object.args.length > MAX_RUNNER_ARGUMENTS) {
345
+ fail('invalid_arguments');
346
+ }
347
+ let totalBytes = 0;
348
+ const args = object.args.map((argument) => {
349
+ if (typeof argument !== 'string'
350
+ || argument.includes('\0')
351
+ || hasUnpairedSurrogate(argument)
352
+ || Buffer.byteLength(argument, 'utf8') > MAX_RUNNER_ARGUMENT_BYTES) {
353
+ fail('invalid_arguments');
354
+ }
355
+ totalBytes += Buffer.byteLength(argument, 'utf8');
356
+ return argument;
357
+ });
358
+ if (totalBytes > MAX_RUNNER_ARGUMENT_VECTOR_BYTES) fail('invalid_arguments');
359
+ return deepFreeze({
360
+ executable: object.executable,
361
+ executable_sha256: executableSha256,
362
+ args,
363
+ });
364
+ }
365
+
366
+ export function parseRefereeRunnerRequest(
367
+ value: unknown,
368
+ ): Readonly<RefereeRunnerRequestV1> {
369
+ const object = exactObject(value, RUNNER_REQUEST_KEYS);
370
+ if (object.version !== REFEREE_RUNNER_REQUEST_VERSION) fail('invalid_version');
371
+ const expectedCaid = object.expected_caid;
372
+ if (typeof expectedCaid !== 'string' || !CAID.test(expectedCaid)) {
373
+ fail('invalid_caid');
374
+ }
375
+ const expectedActionDigest = object.expected_action_digest;
376
+ if (typeof expectedActionDigest !== 'string' || !DIGEST.test(expectedActionDigest)) {
377
+ fail('invalid_action_digest');
378
+ }
379
+ if (typeof object.aec_required !== 'boolean') fail('invalid_aec_required');
380
+ const parsed: RefereeRunnerRequestV1 = {
381
+ version: REFEREE_RUNNER_REQUEST_VERSION,
382
+ case_id: identifier(object.case_id),
383
+ protocol_id: identifier(object.protocol_id),
384
+ expected_caid: expectedCaid,
385
+ expected_action_digest: expectedActionDigest,
386
+ aec_required: object.aec_required,
387
+ execution_scope: enumValue(
388
+ object.execution_scope,
389
+ EXECUTION_SCOPES,
390
+ 'invalid_execution_scope',
391
+ ),
392
+ input: frozenJsonCopy(object.input) as RefereeJson,
393
+ };
394
+ return deepFreeze(parsed);
395
+ }
396
+
397
+ export function parseRefereeRunnerOutput(
398
+ value: unknown,
399
+ ): Readonly<RefereeRunnerOutputV1> {
400
+ const object = exactObject(value, RUNNER_OUTPUT_KEYS);
401
+ if (object.version !== REFEREE_RUNNER_OUTPUT_VERSION) fail('invalid_version');
402
+ const caid = object.caid;
403
+ if (caid !== null && (typeof caid !== 'string' || !CAID.test(caid))) {
404
+ fail('invalid_caid');
405
+ }
406
+ const actionDigest = object.action_digest;
407
+ if (actionDigest !== null
408
+ && (typeof actionDigest !== 'string' || !DIGEST.test(actionDigest))) {
409
+ fail('invalid_action_digest');
410
+ }
411
+ return deepFreeze({
412
+ version: REFEREE_RUNNER_OUTPUT_VERSION,
413
+ case_id: identifier(object.case_id),
414
+ protocol_id: identifier(object.protocol_id),
415
+ native_verification: enumValue(
416
+ object.native_verification,
417
+ NATIVE_VERIFICATIONS,
418
+ 'invalid_native_verification',
419
+ ),
420
+ rp_acceptance: enumValue(
421
+ object.rp_acceptance,
422
+ RP_ACCEPTANCES,
423
+ 'invalid_rp_acceptance',
424
+ ),
425
+ caid,
426
+ action_digest: actionDigest,
427
+ aec_satisfaction: enumValue(
428
+ object.aec_satisfaction,
429
+ AEC_SATISFACTIONS,
430
+ 'invalid_aec_satisfaction',
431
+ ),
432
+ provider_outcome: enumValue(
433
+ object.provider_outcome,
434
+ PROVIDER_OUTCOMES,
435
+ 'invalid_provider_outcome',
436
+ ),
437
+ effect_relation: enumValue(
438
+ object.effect_relation,
439
+ EFFECT_RELATIONS,
440
+ 'invalid_effect_relation',
441
+ ),
442
+ execution_scope: enumValue(
443
+ object.execution_scope,
444
+ EXECUTION_SCOPES,
445
+ 'invalid_execution_scope',
446
+ ),
447
+ });
448
+ }
449
+
450
+ export function parseRefereeEvaluationInput(
451
+ value: unknown,
452
+ ): Readonly<RefereeEvaluationInputV1> {
453
+ const object = exactObject(value, EVALUATION_KEYS);
454
+ if (object.version !== REFEREE_EVALUATION_VERSION) fail('invalid_version');
455
+ return deepFreeze({
456
+ version: REFEREE_EVALUATION_VERSION,
457
+ runner_pin: parseRefereeRunnerPin(object.runner_pin),
458
+ request: parseRefereeRunnerRequest(object.request),
459
+ output: parseRefereeRunnerOutput(object.output),
460
+ });
461
+ }
462
+
463
+ function matchCaidAndAction(
464
+ request: Readonly<RefereeRunnerRequestV1>,
465
+ output: Readonly<RefereeRunnerOutputV1>,
466
+ ): RefereeCaidActionMatch {
467
+ if ((output.caid !== null && output.caid !== request.expected_caid)
468
+ || (output.action_digest !== null
469
+ && output.action_digest !== request.expected_action_digest)) {
470
+ return 'MISMATCH';
471
+ }
472
+ if (output.caid === null || output.action_digest === null) {
473
+ return 'INDETERMINATE';
474
+ }
475
+ return 'MATCH';
476
+ }
477
+
478
+ function resultDimensions(
479
+ request: Readonly<RefereeRunnerRequestV1>,
480
+ output: Readonly<RefereeRunnerOutputV1>,
481
+ ): Readonly<RefereeResultDimensionsV1> {
482
+ return deepFreeze({
483
+ native_verification: { value: output.native_verification },
484
+ rp_acceptance: { value: output.rp_acceptance },
485
+ caid_action_match: {
486
+ value: matchCaidAndAction(request, output),
487
+ expected_caid: request.expected_caid,
488
+ observed_caid: output.caid,
489
+ expected_action_digest: request.expected_action_digest,
490
+ observed_action_digest: output.action_digest,
491
+ },
492
+ aec_satisfaction: {
493
+ required: request.aec_required,
494
+ value: output.aec_satisfaction,
495
+ },
496
+ provider_outcome: { value: output.provider_outcome },
497
+ effect_relation: { value: output.effect_relation },
498
+ });
499
+ }
500
+
501
+ function baseResult(
502
+ runnerPin: Readonly<RefereeRunnerPinV1>,
503
+ request: Readonly<RefereeRunnerRequestV1>,
504
+ status: RefereeStatus,
505
+ dimensions: Readonly<RefereeResultDimensionsV1>,
506
+ reasonCodes: readonly string[],
507
+ ): Readonly<RefereeResultV1> {
508
+ return deepFreeze({
509
+ version: REFEREE_RESULT_VERSION,
510
+ status,
511
+ claim_scope: 'SELF_TEST' as const,
512
+ execution_authorizing: false as const,
513
+ case_id: request.case_id,
514
+ protocol_id: request.protocol_id,
515
+ runner_pin: runnerPin,
516
+ execution_scope: request.execution_scope,
517
+ remote_atomicity_claimed: false as const,
518
+ dimensions,
519
+ reason_codes: [...reasonCodes],
520
+ });
521
+ }
522
+
523
+ /** Evaluate one already-produced, caller-pinned protocol-runner output. */
524
+ export function evaluateReferee(value: unknown): Readonly<RefereeResultV1> {
525
+ const { runner_pin: runnerPin, request, output } = parseRefereeEvaluationInput(value);
526
+ const dimensions = resultDimensions(request, output);
527
+ const reasons: string[] = [];
528
+ let definiteFailure = false;
529
+ let uncertainty = false;
530
+
531
+ const nonConformant = (reason: string) => {
532
+ reasons.push(reason);
533
+ definiteFailure = true;
534
+ };
535
+ const indeterminate = (reason: string) => {
536
+ reasons.push(reason);
537
+ uncertainty = true;
538
+ };
539
+
540
+ if (output.case_id !== request.case_id) nonConformant('case_id_mismatch');
541
+ if (output.protocol_id !== request.protocol_id) nonConformant('protocol_id_mismatch');
542
+ if (output.execution_scope !== request.execution_scope) {
543
+ nonConformant('execution_scope_mismatch');
544
+ }
545
+
546
+ if (dimensions.native_verification.value === 'REJECTED') {
547
+ nonConformant('native_verification_rejected');
548
+ } else if (dimensions.native_verification.value === 'INDETERMINATE') {
549
+ indeterminate('native_verification_indeterminate');
550
+ }
551
+
552
+ if (dimensions.rp_acceptance.value === 'REJECTED') {
553
+ nonConformant('rp_acceptance_rejected');
554
+ } else if (dimensions.rp_acceptance.value === 'INDETERMINATE') {
555
+ indeterminate('rp_acceptance_indeterminate');
556
+ }
557
+
558
+ if (dimensions.caid_action_match.value === 'MISMATCH') {
559
+ nonConformant('caid_action_mismatch');
560
+ } else if (dimensions.caid_action_match.value === 'INDETERMINATE') {
561
+ indeterminate('caid_action_indeterminate');
562
+ }
563
+
564
+ if (dimensions.aec_satisfaction.value === 'NOT_SATISFIED') {
565
+ nonConformant('aec_not_satisfied');
566
+ } else if (dimensions.aec_satisfaction.value === 'INDETERMINATE') {
567
+ indeterminate('aec_indeterminate');
568
+ } else if (request.aec_required
569
+ && dimensions.aec_satisfaction.value === 'NOT_ASSESSED') {
570
+ nonConformant('aec_not_assessed');
571
+ }
572
+
573
+ if (dimensions.provider_outcome.value === 'INDETERMINATE') {
574
+ indeterminate('provider_outcome_indeterminate');
575
+ }
576
+
577
+ if (dimensions.effect_relation.value === 'DIVERGED') {
578
+ nonConformant('effect_diverged');
579
+ } else if (dimensions.effect_relation.value === 'INDETERMINATE') {
580
+ indeterminate('effect_indeterminate');
581
+ }
582
+
583
+ const status: RefereeStatus = definiteFailure
584
+ ? 'NON_CONFORMANT'
585
+ : uncertainty
586
+ ? 'INDETERMINATE'
587
+ : 'CONFORMANT';
588
+ return baseResult(runnerPin, request, status, dimensions, reasons);
589
+ }
590
+
591
+ /** Convert a bounded runner failure into an explicit no-claim result. */
592
+ export function createIndeterminateRefereeResult(
593
+ value: RefereeIndeterminateInputV1,
594
+ ): Readonly<RefereeResultV1> {
595
+ const object = exactObject(value, ['runner_pin', 'request', 'reason_code'] as const);
596
+ const runnerPin = parseRefereeRunnerPin(object.runner_pin);
597
+ const request = parseRefereeRunnerRequest(object.request);
598
+ if (typeof object.reason_code !== 'string' || !REASON_CODE.test(object.reason_code)) {
599
+ fail('invalid_reason_code');
600
+ }
601
+ const dimensions: Readonly<RefereeResultDimensionsV1> = deepFreeze({
602
+ native_verification: { value: 'INDETERMINATE' },
603
+ rp_acceptance: { value: 'INDETERMINATE' },
604
+ caid_action_match: {
605
+ value: 'INDETERMINATE',
606
+ expected_caid: request.expected_caid,
607
+ observed_caid: null,
608
+ expected_action_digest: request.expected_action_digest,
609
+ observed_action_digest: null,
610
+ },
611
+ aec_satisfaction: {
612
+ required: request.aec_required,
613
+ value: request.aec_required ? 'INDETERMINATE' : 'NOT_ASSESSED',
614
+ },
615
+ provider_outcome: { value: 'NOT_ASSESSED' },
616
+ effect_relation: { value: 'NOT_ASSESSED' },
617
+ });
618
+ return baseResult(
619
+ runnerPin,
620
+ request,
621
+ 'INDETERMINATE',
622
+ dimensions,
623
+ [object.reason_code],
624
+ );
625
+ }