@nifrajs/agent-app 3.2.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.
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/transport.d.ts +5 -2
- package/dist/transport.d.ts.map +1 -1
- package/dist/transport.js +117 -12
- package/dist/transport.js.map +1 -1
- package/dist/view-models.d.ts +58 -0
- package/dist/view-models.d.ts.map +1 -1
- package/dist/view-models.js +442 -1
- package/dist/view-models.js.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +1 -0
- package/src/index.ts +6 -0
- package/src/transport.ts +125 -12
- package/src/view-models.ts +563 -0
package/src/view-models.ts
CHANGED
|
@@ -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
|
+
}
|