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