@nifrajs/agent-app 3.3.0 → 3.5.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.
@@ -18,6 +18,7 @@ import {
18
18
  type BoundaryCommand,
19
19
  type HandoffLifecycleState,
20
20
  type HandoffSnapshot,
21
+ isAgentEvent,
21
22
  nextApprovalState,
22
23
  nextHandoffState,
23
24
  type RunSnapshot,
@@ -172,6 +173,10 @@ export type AgentEventView =
172
173
 
173
174
  /** Project one protocol event to its content-free view. Total over the event union - never returns undefined. */
174
175
  export function toEventView(event: AgentEvent): AgentEventView {
176
+ // Keep this boundary total even when a caller bypasses the transport's parser with an
177
+ // untrusted cast. The protocol validator is discriminated and bounded; throwing only this
178
+ // generic message prevents malformed payloads from reaching content projections or error text.
179
+ if (!isAgentEvent(event)) throw new TypeError("agent event is invalid")
175
180
  const base = { seq: event.seq, at: event.at }
176
181
  switch (event.type) {
177
182
  case "session.started":
@@ -917,3 +922,561 @@ export function virtualizeEvidenceRows<T>(
917
922
  )
918
923
  return Object.freeze({ offset, rows: Object.freeze(rows.slice(offset, offset + windowSize)) })
919
924
  }
925
+
926
+ // ---------------------------------------------------------------------------
927
+ // Review projections
928
+ // ---------------------------------------------------------------------------
929
+
930
+ export type ReviewViewStatus = "pass" | "fail" | "inconclusive" | "unavailable"
931
+
932
+ export interface ReviewViewEvidence {
933
+ readonly source: string
934
+ readonly token: string
935
+ readonly digest: string
936
+ readonly path?: string
937
+ }
938
+
939
+ export interface ReviewViewFinding {
940
+ readonly id: string
941
+ readonly check: string
942
+ readonly code: string
943
+ readonly severity: "error" | "warning" | "info"
944
+ readonly category: string
945
+ readonly location?: { readonly path: string; readonly line?: number }
946
+ readonly evidence: readonly ReviewViewEvidence[]
947
+ readonly fix?: { readonly recipe: string }
948
+ }
949
+
950
+ export interface ReviewViewCheck {
951
+ readonly id: string
952
+ readonly required: boolean
953
+ readonly status: "pass" | "fail" | "skipped" | "unavailable" | "error"
954
+ readonly findings: number
955
+ readonly errors: number
956
+ readonly warnings: number
957
+ readonly info: number
958
+ readonly outOfScope: number
959
+ readonly reasonCode?: string
960
+ }
961
+
962
+ export interface ReviewView {
963
+ readonly version: 1
964
+ readonly status: ReviewViewStatus
965
+ readonly ok: boolean
966
+ readonly strict: boolean
967
+ readonly blocking: number
968
+ readonly digest?: string
969
+ readonly processStatus?: number | null
970
+ readonly reasonCode?: "invalid-report" | "unavailable" | "process-failed"
971
+ readonly scope?: {
972
+ readonly kind: "project" | "diff"
973
+ readonly state: "valid" | "invalid"
974
+ readonly pathDigest: string
975
+ readonly changedPathCount: number
976
+ readonly outOfScopeCount: number
977
+ }
978
+ readonly checks: readonly ReviewViewCheck[]
979
+ readonly findings: readonly ReviewViewFinding[]
980
+ readonly fixes: readonly {
981
+ readonly recipe: string
982
+ readonly status: "planned" | "changed" | "no-op" | "failed"
983
+ }[]
984
+ }
985
+
986
+ const REVIEW_VIEW_CODE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/
987
+ const REVIEW_VIEW_TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/
988
+ const REVIEW_VIEW_DIGEST = /^[0-9a-f]{64}$/
989
+ const REVIEW_VIEW_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
990
+ const REVIEW_VIEW_EVIDENCE_SOURCES = new Set([
991
+ "check",
992
+ "assurance",
993
+ "capability",
994
+ "manifest",
995
+ "contract",
996
+ "coverage",
997
+ "git-scope",
998
+ "collector",
999
+ ])
1000
+ const REVIEW_VIEW_REASONS = new Set([
1001
+ "not-configured",
1002
+ "config-missing",
1003
+ "config-invalid",
1004
+ "collector-error",
1005
+ "collector-unavailable",
1006
+ "diagnostics-truncated",
1007
+ "missing-typescript",
1008
+ "invalid-target",
1009
+ "invalid-git-ref",
1010
+ "invalid-git-scope",
1011
+ "filtered-out-of-scope",
1012
+ "fix-failed",
1013
+ "fix-no-op",
1014
+ "unsupported",
1015
+ ])
1016
+ const REVIEW_VIEW_FIX_RECIPES = new Set(["manifest.sync", "workspace-dist.rebuild"])
1017
+ const REVIEW_VIEW_FIX_STATUSES = new Set(["planned", "changed", "no-op", "failed"])
1018
+ const REVIEW_VIEW_RPC_ERRORS = new Set([
1019
+ "timeout",
1020
+ "output-truncated",
1021
+ "invalid-report",
1022
+ "spawn-failed",
1023
+ ])
1024
+ const REVIEW_VIEW_CHECKS = new Set([
1025
+ "typecheck",
1026
+ "typed-client",
1027
+ "server-boundary",
1028
+ "route-boundary",
1029
+ "pipeline",
1030
+ "security",
1031
+ "route-assurance",
1032
+ "capability-provenance",
1033
+ "manifest",
1034
+ "dependency",
1035
+ "contract-witness",
1036
+ "coverage",
1037
+ "hydration",
1038
+ "configuration",
1039
+ ])
1040
+ const REVIEW_VIEW_STATUSES = new Set(["pass", "fail", "skipped", "unavailable", "error"])
1041
+ const REVIEW_VIEW_SEVERITIES = new Set(["error", "warning", "info"])
1042
+ const REVIEW_VIEW_CATEGORIES = new Set([
1043
+ "correctness",
1044
+ "security",
1045
+ "boundary",
1046
+ "assurance",
1047
+ "capability",
1048
+ "contract",
1049
+ "dependency",
1050
+ "hydration",
1051
+ "coverage",
1052
+ "configuration",
1053
+ "operational",
1054
+ ])
1055
+ const REVIEW_VIEW_FORBIDDEN = new Set([
1056
+ "prompt",
1057
+ "message",
1058
+ "text",
1059
+ "input",
1060
+ "output",
1061
+ "arguments",
1062
+ "body",
1063
+ "response",
1064
+ "secret",
1065
+ "credential",
1066
+ "diagnostic",
1067
+ "stack",
1068
+ "content",
1069
+ "transcript",
1070
+ "request",
1071
+ "payload",
1072
+ ])
1073
+
1074
+ function reviewViewRecord(value: unknown): value is Record<string, unknown> {
1075
+ return typeof value === "object" && value !== null && !Array.isArray(value)
1076
+ }
1077
+
1078
+ function reviewViewSafeKeys(value: Record<string, unknown>, allowed: ReadonlySet<string>): boolean {
1079
+ return Object.keys(value).every(
1080
+ (key) => !REVIEW_VIEW_FORBIDDEN.has(key.toLowerCase()) && allowed.has(key),
1081
+ )
1082
+ }
1083
+
1084
+ function reviewViewEvidence(value: unknown): value is ReviewViewEvidence {
1085
+ if (!reviewViewRecord(value)) return false
1086
+ return (
1087
+ reviewViewSafeKeys(value, new Set(["source", "token", "digest", "path"])) &&
1088
+ typeof value.source === "string" &&
1089
+ REVIEW_VIEW_EVIDENCE_SOURCES.has(value.source) &&
1090
+ typeof value.token === "string" &&
1091
+ REVIEW_VIEW_TOKEN.test(value.token) &&
1092
+ typeof value.digest === "string" &&
1093
+ REVIEW_VIEW_DIGEST.test(value.digest) &&
1094
+ (value.path === undefined || reviewViewPath(value.path))
1095
+ )
1096
+ }
1097
+
1098
+ function reviewViewReason(value: unknown): value is string {
1099
+ return typeof value === "string" && REVIEW_VIEW_REASONS.has(value)
1100
+ }
1101
+
1102
+ function reviewViewPath(value: unknown): value is string {
1103
+ return (
1104
+ typeof value === "string" &&
1105
+ value.length > 0 &&
1106
+ value.length <= 1024 &&
1107
+ !value.includes("\\") &&
1108
+ !value.includes("\u0000") &&
1109
+ !value.startsWith("/") &&
1110
+ !/^[A-Za-z]:/.test(value) &&
1111
+ value.split("/").every((part) => part.length > 0 && part !== "." && part !== "..")
1112
+ )
1113
+ }
1114
+
1115
+ function reviewViewInt(value: unknown, min = 0): value is number {
1116
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= min
1117
+ }
1118
+
1119
+ function reviewUnavailableView(
1120
+ processStatus: number | null,
1121
+ reasonCode: "invalid-report" | "unavailable" | "process-failed",
1122
+ ): ReviewView {
1123
+ return Object.freeze({
1124
+ version: 1,
1125
+ status: "unavailable",
1126
+ ok: false,
1127
+ strict: false,
1128
+ blocking: 0,
1129
+ processStatus,
1130
+ reasonCode,
1131
+ checks: Object.freeze([]),
1132
+ findings: Object.freeze([]),
1133
+ fixes: Object.freeze([]),
1134
+ })
1135
+ }
1136
+
1137
+ function parseReviewViewReport(value: Record<string, unknown>): ReviewView | undefined {
1138
+ if (
1139
+ !reviewViewSafeKeys(
1140
+ value,
1141
+ new Set([
1142
+ "version",
1143
+ "strict",
1144
+ "scope",
1145
+ "checks",
1146
+ "findings",
1147
+ "fixes",
1148
+ "blocking",
1149
+ "ok",
1150
+ "status",
1151
+ "digest",
1152
+ ]),
1153
+ ) ||
1154
+ value.version !== 1 ||
1155
+ typeof value.strict !== "boolean" ||
1156
+ (value.status !== "pass" && value.status !== "fail" && value.status !== "inconclusive") ||
1157
+ typeof value.ok !== "boolean" ||
1158
+ value.ok !== (value.status === "pass") ||
1159
+ !reviewViewInt(value.blocking) ||
1160
+ typeof value.digest !== "string" ||
1161
+ !REVIEW_VIEW_DIGEST.test(value.digest) ||
1162
+ !Array.isArray(value.checks) ||
1163
+ !Array.isArray(value.findings) ||
1164
+ value.scope === undefined
1165
+ )
1166
+ return undefined
1167
+
1168
+ let scope: ReviewView["scope"]
1169
+ if (value.scope !== undefined) {
1170
+ if (
1171
+ !reviewViewRecord(value.scope) ||
1172
+ !reviewViewSafeKeys(
1173
+ value.scope,
1174
+ new Set([
1175
+ "kind",
1176
+ "state",
1177
+ "gitRef",
1178
+ "changedPaths",
1179
+ "pathDigest",
1180
+ "outOfScopeCount",
1181
+ "reasonCode",
1182
+ ]),
1183
+ ) ||
1184
+ (value.scope.kind !== "project" && value.scope.kind !== "diff") ||
1185
+ (value.scope.state !== "valid" && value.scope.state !== "invalid") ||
1186
+ !Array.isArray(value.scope.changedPaths) ||
1187
+ !value.scope.changedPaths.every(reviewViewPath) ||
1188
+ typeof value.scope.pathDigest !== "string" ||
1189
+ !REVIEW_VIEW_DIGEST.test(value.scope.pathDigest) ||
1190
+ !reviewViewInt(value.scope.outOfScopeCount)
1191
+ )
1192
+ return undefined
1193
+ if (value.scope.gitRef !== undefined && !REVIEW_VIEW_TOKEN.test(String(value.scope.gitRef)))
1194
+ return undefined
1195
+ if (value.scope.state === "invalid") {
1196
+ if (!reviewViewReason(value.scope.reasonCode)) return undefined
1197
+ } else if (value.scope.reasonCode !== undefined) return undefined
1198
+ scope = Object.freeze({
1199
+ kind: value.scope.kind,
1200
+ state: value.scope.state,
1201
+ pathDigest: value.scope.pathDigest,
1202
+ changedPathCount: value.scope.changedPaths.length,
1203
+ outOfScopeCount: value.scope.outOfScopeCount,
1204
+ })
1205
+ }
1206
+
1207
+ if (value.checks.length > 512 || value.findings.length > 4096) return undefined
1208
+ const checks: ReviewViewCheck[] = []
1209
+ const checkFindingIds = new Map<string, readonly string[]>()
1210
+ for (const raw of value.checks) {
1211
+ if (
1212
+ !reviewViewRecord(raw) ||
1213
+ !reviewViewSafeKeys(
1214
+ raw,
1215
+ new Set([
1216
+ "id",
1217
+ "required",
1218
+ "status",
1219
+ "duration",
1220
+ "counts",
1221
+ "findingIds",
1222
+ "evidence",
1223
+ "reasonCode",
1224
+ ]),
1225
+ ) ||
1226
+ typeof raw.id !== "string" ||
1227
+ !REVIEW_VIEW_CHECKS.has(raw.id) ||
1228
+ checks.some((check) => check.id === raw.id) ||
1229
+ typeof raw.required !== "boolean" ||
1230
+ typeof raw.status !== "string" ||
1231
+ !REVIEW_VIEW_STATUSES.has(raw.status) ||
1232
+ !reviewViewRecord(raw.counts) ||
1233
+ !reviewViewSafeKeys(
1234
+ raw.counts,
1235
+ new Set(["findings", "errors", "warnings", "info", "outOfScope"]),
1236
+ ) ||
1237
+ !reviewViewInt(raw.counts.findings) ||
1238
+ !reviewViewInt(raw.counts.errors) ||
1239
+ !reviewViewInt(raw.counts.warnings) ||
1240
+ !reviewViewInt(raw.counts.info) ||
1241
+ !reviewViewInt(raw.counts.outOfScope) ||
1242
+ !Array.isArray(raw.findingIds) ||
1243
+ !raw.findingIds.every((id) => typeof id === "string" && REVIEW_VIEW_ID.test(id)) ||
1244
+ new Set(raw.findingIds).size !== raw.findingIds.length ||
1245
+ !Array.isArray(raw.evidence) ||
1246
+ raw.evidence.length === 0 ||
1247
+ raw.evidence.length > 1024 ||
1248
+ !raw.evidence.every(reviewViewEvidence)
1249
+ )
1250
+ return undefined
1251
+ if (raw.status === "skipped") {
1252
+ if (raw.required || raw.reasonCode !== "not-configured") return undefined
1253
+ } else if (raw.status === "unavailable" || raw.status === "error") {
1254
+ if (!reviewViewReason(raw.reasonCode)) return undefined
1255
+ } else if (raw.reasonCode !== undefined) return undefined
1256
+ checkFindingIds.set(raw.id, [...raw.findingIds])
1257
+ checks.push(
1258
+ Object.freeze({
1259
+ id: raw.id,
1260
+ required: raw.required,
1261
+ status: raw.status as ReviewViewCheck["status"],
1262
+ findings: raw.counts.findings,
1263
+ errors: raw.counts.errors,
1264
+ warnings: raw.counts.warnings,
1265
+ info: raw.counts.info,
1266
+ outOfScope: raw.counts.outOfScope,
1267
+ ...(raw.reasonCode === undefined ? {} : { reasonCode: raw.reasonCode as string }),
1268
+ }),
1269
+ )
1270
+ }
1271
+
1272
+ const findings: ReviewViewFinding[] = []
1273
+ for (const raw of value.findings) {
1274
+ if (
1275
+ !reviewViewRecord(raw) ||
1276
+ !reviewViewSafeKeys(
1277
+ raw,
1278
+ new Set(["id", "check", "code", "severity", "category", "location", "evidence", "fix"]),
1279
+ ) ||
1280
+ typeof raw.id !== "string" ||
1281
+ !REVIEW_VIEW_ID.test(raw.id) ||
1282
+ findings.some((finding) => finding.id === raw.id) ||
1283
+ typeof raw.check !== "string" ||
1284
+ !REVIEW_VIEW_CHECKS.has(raw.check) ||
1285
+ typeof raw.code !== "string" ||
1286
+ !REVIEW_VIEW_CODE.test(raw.code) ||
1287
+ typeof raw.severity !== "string" ||
1288
+ !REVIEW_VIEW_SEVERITIES.has(raw.severity) ||
1289
+ typeof raw.category !== "string" ||
1290
+ !REVIEW_VIEW_CATEGORIES.has(raw.category) ||
1291
+ !Array.isArray(raw.evidence) ||
1292
+ raw.evidence.length === 0 ||
1293
+ raw.evidence.length > 32
1294
+ )
1295
+ return undefined
1296
+ let location: ReviewViewFinding["location"]
1297
+ if (raw.location !== undefined) {
1298
+ if (
1299
+ !reviewViewRecord(raw.location) ||
1300
+ !reviewViewSafeKeys(raw.location, new Set(["path", "line", "column"])) ||
1301
+ !reviewViewPath(raw.location.path) ||
1302
+ (raw.location.line !== undefined && !reviewViewInt(raw.location.line, 1)) ||
1303
+ (raw.location.column !== undefined && !reviewViewInt(raw.location.column, 1))
1304
+ )
1305
+ return undefined
1306
+ location = Object.freeze({
1307
+ path: raw.location.path,
1308
+ ...(raw.location.line === undefined ? {} : { line: raw.location.line }),
1309
+ })
1310
+ }
1311
+ const evidence: ReviewViewEvidence[] = []
1312
+ for (const rawEvidence of raw.evidence) {
1313
+ if (!reviewViewEvidence(rawEvidence)) return undefined
1314
+ evidence.push(
1315
+ Object.freeze({
1316
+ source: rawEvidence.source,
1317
+ token: rawEvidence.token,
1318
+ digest: rawEvidence.digest,
1319
+ ...(rawEvidence.path === undefined ? {} : { path: rawEvidence.path }),
1320
+ }),
1321
+ )
1322
+ }
1323
+ let fix: ReviewViewFinding["fix"]
1324
+ if (raw.fix !== undefined) {
1325
+ if (
1326
+ !reviewViewRecord(raw.fix) ||
1327
+ !reviewViewSafeKeys(raw.fix, new Set(["recipe"])) ||
1328
+ typeof raw.fix.recipe !== "string" ||
1329
+ !REVIEW_VIEW_FIX_RECIPES.has(raw.fix.recipe)
1330
+ )
1331
+ return undefined
1332
+ fix = Object.freeze({ recipe: raw.fix.recipe })
1333
+ }
1334
+ findings.push(
1335
+ Object.freeze({
1336
+ id: raw.id,
1337
+ check: raw.check,
1338
+ code: raw.code,
1339
+ severity: raw.severity as ReviewViewFinding["severity"],
1340
+ category: raw.category,
1341
+ ...(location === undefined ? {} : { location }),
1342
+ evidence: Object.freeze(evidence),
1343
+ ...(fix === undefined ? {} : { fix }),
1344
+ }),
1345
+ )
1346
+ }
1347
+
1348
+ const fixes: Array<ReviewView["fixes"][number]> = []
1349
+ if (value.fixes !== undefined) {
1350
+ if (!Array.isArray(value.fixes) || value.fixes.length > 256) return undefined
1351
+ for (const raw of value.fixes) {
1352
+ if (
1353
+ !reviewViewRecord(raw) ||
1354
+ !reviewViewSafeKeys(raw, new Set(["recipe", "status", "changedPaths", "reasonCode"])) ||
1355
+ typeof raw.recipe !== "string" ||
1356
+ !REVIEW_VIEW_FIX_RECIPES.has(raw.recipe) ||
1357
+ typeof raw.status !== "string" ||
1358
+ !REVIEW_VIEW_FIX_STATUSES.has(raw.status) ||
1359
+ (raw.changedPaths !== undefined &&
1360
+ (!Array.isArray(raw.changedPaths) || !raw.changedPaths.every(reviewViewPath)))
1361
+ )
1362
+ return undefined
1363
+ if (raw.status === "no-op" || raw.status === "failed") {
1364
+ if (!reviewViewReason(raw.reasonCode)) return undefined
1365
+ } else if (raw.reasonCode !== undefined) return undefined
1366
+ fixes.push(
1367
+ Object.freeze({
1368
+ recipe: raw.recipe,
1369
+ status: raw.status as (typeof fixes)[number]["status"],
1370
+ }),
1371
+ )
1372
+ }
1373
+ }
1374
+
1375
+ const findingById = new Map(findings.map((finding) => [finding.id, finding]))
1376
+ const referenced = new Set<string>()
1377
+ for (const check of checks) {
1378
+ const ids = checkFindingIds.get(check.id) ?? []
1379
+ const ownFindings = findings.filter((finding) => finding.check === check.id)
1380
+ if (
1381
+ ownFindings.length !== ids.length ||
1382
+ check.findings !== ownFindings.length ||
1383
+ check.errors !== ownFindings.filter((finding) => finding.severity === "error").length ||
1384
+ check.warnings !== ownFindings.filter((finding) => finding.severity === "warning").length ||
1385
+ check.info !== ownFindings.filter((finding) => finding.severity === "info").length
1386
+ )
1387
+ return undefined
1388
+ for (const id of ids) {
1389
+ const finding = findingById.get(id)
1390
+ if (finding === undefined || finding.check !== check.id || referenced.has(id))
1391
+ return undefined
1392
+ referenced.add(id)
1393
+ }
1394
+ }
1395
+ if (referenced.size !== findings.length) return undefined
1396
+
1397
+ const errorCount = findings.filter((finding) => finding.severity === "error").length
1398
+ const warningCount = findings.filter((finding) => finding.severity === "warning").length
1399
+ const unresolvedFixes = fixes.filter(
1400
+ (fix) => fix.status === "no-op" || fix.status === "failed",
1401
+ ).length
1402
+ const expectedBlocking = errorCount + (value.strict ? warningCount : 0) + unresolvedFixes
1403
+ const expectedInconclusive =
1404
+ value.scope.state === "invalid" ||
1405
+ checks.some(
1406
+ (check) => check.required && (check.status === "unavailable" || check.status === "error"),
1407
+ )
1408
+ const expectedStatus = expectedInconclusive
1409
+ ? "inconclusive"
1410
+ : expectedBlocking > 0
1411
+ ? "fail"
1412
+ : "pass"
1413
+ if (value.blocking !== expectedBlocking || value.status !== expectedStatus) return undefined
1414
+
1415
+ checks.sort((left, right) => left.id.localeCompare(right.id))
1416
+ findings.sort((left, right) => left.id.localeCompare(right.id))
1417
+ return Object.freeze({
1418
+ version: 1,
1419
+ status: value.status,
1420
+ ok: value.ok,
1421
+ strict: value.strict,
1422
+ blocking: value.blocking,
1423
+ digest: value.digest,
1424
+ ...(scope === undefined ? {} : { scope }),
1425
+ checks: Object.freeze(checks),
1426
+ findings: Object.freeze(findings),
1427
+ fixes: Object.freeze(fixes),
1428
+ })
1429
+ }
1430
+
1431
+ /** Project a review report or host RPC result into a bounded, content-free browser view. */
1432
+ export function toReviewView(value: unknown): ReviewView | undefined {
1433
+ if (!reviewViewRecord(value)) return undefined
1434
+ if (
1435
+ !Object.hasOwn(value, "report") &&
1436
+ Object.hasOwn(value, "ok") &&
1437
+ Object.hasOwn(value, "status") &&
1438
+ (value.status === null || typeof value.status === "number")
1439
+ ) {
1440
+ if (
1441
+ !reviewViewSafeKeys(value, new Set(["ok", "status", "errorCode"])) ||
1442
+ typeof value.ok !== "boolean" ||
1443
+ (value.status !== null && !reviewViewInt(value.status)) ||
1444
+ (value.errorCode !== undefined &&
1445
+ (typeof value.errorCode !== "string" || !REVIEW_VIEW_RPC_ERRORS.has(value.errorCode)))
1446
+ )
1447
+ return reviewUnavailableView(null, "invalid-report")
1448
+ const reason =
1449
+ value.errorCode === "invalid-report"
1450
+ ? "invalid-report"
1451
+ : value.errorCode === undefined
1452
+ ? value.ok
1453
+ ? "invalid-report"
1454
+ : "unavailable"
1455
+ : "process-failed"
1456
+ return reviewUnavailableView(value.status as number | null, reason)
1457
+ }
1458
+ if (Object.hasOwn(value, "report")) {
1459
+ if (
1460
+ !reviewViewSafeKeys(value, new Set(["ok", "status", "report", "errorCode"])) ||
1461
+ (value.status !== null && !reviewViewInt(value.status)) ||
1462
+ typeof value.ok !== "boolean" ||
1463
+ (value.errorCode !== undefined &&
1464
+ (typeof value.errorCode !== "string" || !REVIEW_VIEW_RPC_ERRORS.has(value.errorCode))) ||
1465
+ value.errorCode !== undefined
1466
+ )
1467
+ return reviewUnavailableView(null, "invalid-report")
1468
+ if (value.report === undefined)
1469
+ return reviewUnavailableView(
1470
+ value.status as number | null,
1471
+ value.ok ? "invalid-report" : "unavailable",
1472
+ )
1473
+ const report = toReviewView(value.report)
1474
+ return report === undefined
1475
+ ? reviewUnavailableView(value.status as number | null, "invalid-report")
1476
+ : Object.freeze({
1477
+ ...report,
1478
+ ...(value.status === undefined ? {} : { processStatus: value.status as number | null }),
1479
+ })
1480
+ }
1481
+ return parseReviewViewReport(value)
1482
+ }