@nifrajs/agent-review 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/parser.js ADDED
@@ -0,0 +1,664 @@
1
+ import { canonicalizeReviewReport, digestReviewReport } from "./canonical.js";
2
+ import { REVIEW_REPORT_VERSION, } from "./types.js";
3
+ export const REVIEW_MAX_BYTES = 256 * 1024;
4
+ export const REVIEW_MAX_DEPTH = 8;
5
+ export const REVIEW_MAX_OBJECT_KEYS = 32;
6
+ export const REVIEW_MAX_ARRAY_ITEMS = 1024;
7
+ export const REVIEW_MAX_PATH_BYTES = 512;
8
+ export const REVIEW_MAX_REF_BYTES = 256;
9
+ export const REVIEW_MAX_ID_BYTES = 128;
10
+ export const REVIEW_MAX_CODE_BYTES = 64;
11
+ const HEX64 = /^[0-9a-f]{64}$/;
12
+ const ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
13
+ const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/;
14
+ const CODE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
15
+ const GIT_REF_FORBIDDEN = /[\s\\~^:?*[\]]/;
16
+ const FORBIDDEN_KEYS = new Set([
17
+ "message",
18
+ "description",
19
+ "title",
20
+ "prompt",
21
+ "output",
22
+ "payload",
23
+ "snapshot",
24
+ "diagnostic",
25
+ "suggestion",
26
+ "chain",
27
+ "stack",
28
+ "input",
29
+ "body",
30
+ "secret",
31
+ ]);
32
+ const REVIEW_CHECK_IDS = [
33
+ "typecheck",
34
+ "typed-client",
35
+ "server-boundary",
36
+ "route-boundary",
37
+ "pipeline",
38
+ "security",
39
+ "route-assurance",
40
+ "capability-provenance",
41
+ "manifest",
42
+ "dependency",
43
+ "contract-witness",
44
+ "coverage",
45
+ "hydration",
46
+ "configuration",
47
+ ];
48
+ const REVIEW_CHECK_STATUSES = [
49
+ "pass",
50
+ "fail",
51
+ "skipped",
52
+ "unavailable",
53
+ "error",
54
+ ];
55
+ const REVIEW_SEVERITIES = ["error", "warning", "info"];
56
+ const REVIEW_CATEGORIES = [
57
+ "correctness",
58
+ "security",
59
+ "boundary",
60
+ "assurance",
61
+ "capability",
62
+ "contract",
63
+ "dependency",
64
+ "hydration",
65
+ "coverage",
66
+ "configuration",
67
+ "operational",
68
+ ];
69
+ const REVIEW_EVIDENCE_SOURCES = [
70
+ "check",
71
+ "assurance",
72
+ "capability",
73
+ "manifest",
74
+ "contract",
75
+ "coverage",
76
+ "git-scope",
77
+ "collector",
78
+ ];
79
+ const REVIEW_DURATIONS = [
80
+ "none",
81
+ "fast",
82
+ "standard",
83
+ "slow",
84
+ "timeout",
85
+ ];
86
+ const REVIEW_REASONS = [
87
+ "not-configured",
88
+ "config-missing",
89
+ "config-invalid",
90
+ "collector-error",
91
+ "collector-unavailable",
92
+ "diagnostics-truncated",
93
+ "missing-typescript",
94
+ "invalid-target",
95
+ "invalid-git-ref",
96
+ "invalid-git-scope",
97
+ "filtered-out-of-scope",
98
+ "fix-failed",
99
+ "fix-no-op",
100
+ "unsupported",
101
+ ];
102
+ const REVIEW_FIX_RECIPES = ["manifest.sync", "workspace-dist.rebuild"];
103
+ const REVIEW_FIX_STATUSES = ["planned", "changed", "no-op", "failed"];
104
+ /** Error raised for every malformed, unsafe, or payload-bearing report. */
105
+ export class ReviewParserError extends Error {
106
+ code;
107
+ path;
108
+ constructor(code, path) {
109
+ super(`invalid review report (${code})${path === undefined ? "" : ` at ${path}`}`);
110
+ this.name = "ReviewParserError";
111
+ this.code = code;
112
+ if (path !== undefined)
113
+ this.path = path;
114
+ }
115
+ }
116
+ function fail(code, path) {
117
+ throw new ReviewParserError(code, path);
118
+ }
119
+ function isPlainRecord(value) {
120
+ if (typeof value !== "object" || value === null || Array.isArray(value))
121
+ return false;
122
+ try {
123
+ const prototype = Object.getPrototypeOf(value);
124
+ return prototype === Object.prototype || prototype === null;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ function hasOwn(record, key) {
131
+ return Object.hasOwn(record, key);
132
+ }
133
+ function compareText(left, right) {
134
+ return left < right ? -1 : left > right ? 1 : 0;
135
+ }
136
+ class ParseGuard {
137
+ seen = new WeakSet();
138
+ chargedBytes = 0;
139
+ enter(value, depth, path) {
140
+ if (depth > REVIEW_MAX_DEPTH)
141
+ fail("oversized", path);
142
+ if (this.seen.has(value))
143
+ fail("invalid-reference", path);
144
+ this.seen.add(value);
145
+ }
146
+ chargeString(value, maxBytes, code, path) {
147
+ const bytes = new TextEncoder().encode(value).byteLength;
148
+ if (bytes === 0 || bytes > maxBytes)
149
+ fail(code, path);
150
+ if (value.includes("\u0000") || value.includes("\\"))
151
+ fail(code, path);
152
+ this.chargedBytes += bytes + 2;
153
+ if (this.chargedBytes > REVIEW_MAX_BYTES)
154
+ fail("oversized", path);
155
+ }
156
+ }
157
+ function recordAt(value, guard, depth, path) {
158
+ if (!isPlainRecord(value))
159
+ fail("invalid-type", path);
160
+ guard.enter(value, depth, path);
161
+ return value;
162
+ }
163
+ function arrayAt(value, guard, depth, path) {
164
+ if (!Array.isArray(value))
165
+ fail("invalid-type", path);
166
+ if (value.length > REVIEW_MAX_ARRAY_ITEMS)
167
+ fail("oversized", path);
168
+ guard.enter(value, depth, path);
169
+ return value;
170
+ }
171
+ function assertKeys(record, allowed, guard, path, allowSource = false) {
172
+ const allowedSet = new Set(allowed);
173
+ let ownKeys;
174
+ try {
175
+ ownKeys = Reflect.ownKeys(record);
176
+ }
177
+ catch {
178
+ fail("invalid-type", path);
179
+ }
180
+ if (ownKeys.length > REVIEW_MAX_OBJECT_KEYS)
181
+ fail("oversized", path);
182
+ for (const key of ownKeys) {
183
+ if (typeof key !== "string")
184
+ fail("unknown-key", path);
185
+ guard.chargeString(key, REVIEW_MAX_ID_BYTES, "unknown-key", path);
186
+ const lower = key.toLowerCase();
187
+ if (lower === "source" && (!allowSource || key !== "source"))
188
+ fail("forbidden-content", path);
189
+ if (FORBIDDEN_KEYS.has(lower))
190
+ fail("forbidden-content", path);
191
+ if (!allowedSet.has(key))
192
+ fail("unknown-key", path);
193
+ }
194
+ }
195
+ function requiredValue(record, key, path) {
196
+ if (!hasOwn(record, key) || record[key] === undefined)
197
+ fail("missing-required", `${path}.${key}`);
198
+ return record[key];
199
+ }
200
+ function optionalValue(record, key, path) {
201
+ if (!hasOwn(record, key))
202
+ return undefined;
203
+ if (record[key] === undefined)
204
+ fail("invalid-type", `${path}.${key}`);
205
+ return record[key];
206
+ }
207
+ function stringValue(value, guard, path, maxBytes, code = "invalid-type") {
208
+ if (typeof value !== "string")
209
+ fail("invalid-type", path);
210
+ guard.chargeString(value, maxBytes, code, path);
211
+ return value;
212
+ }
213
+ function optionalString(record, key, guard, path, maxBytes, code = "invalid-type") {
214
+ const value = optionalValue(record, key, path);
215
+ return value === undefined
216
+ ? undefined
217
+ : stringValue(value, guard, `${path}.${key}`, maxBytes, code);
218
+ }
219
+ function requiredString(record, key, guard, path, maxBytes, code = "invalid-type") {
220
+ return stringValue(requiredValue(record, key, path), guard, `${path}.${key}`, maxBytes, code);
221
+ }
222
+ function booleanValue(record, key, path) {
223
+ const value = requiredValue(record, key, path);
224
+ if (typeof value !== "boolean")
225
+ fail("invalid-type", `${path}.${key}`);
226
+ return value;
227
+ }
228
+ function integerValue(record, key, path) {
229
+ const value = requiredValue(record, key, path);
230
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
231
+ fail("invalid-type", `${path}.${key}`);
232
+ return value;
233
+ }
234
+ function enumValue(record, key, values, guard, path) {
235
+ const value = requiredString(record, key, guard, path, REVIEW_MAX_ID_BYTES);
236
+ if (!values.includes(value))
237
+ fail("invalid-enum", `${path}.${key}`);
238
+ return value;
239
+ }
240
+ function optionalEnum(record, key, values, guard, path) {
241
+ const value = optionalString(record, key, guard, path, REVIEW_MAX_ID_BYTES);
242
+ if (value === undefined)
243
+ return undefined;
244
+ if (!values.includes(value))
245
+ fail("invalid-enum", `${path}.${key}`);
246
+ return value;
247
+ }
248
+ function digestValue(record, key, guard, path) {
249
+ const value = requiredString(record, key, guard, path, REVIEW_MAX_CODE_BYTES);
250
+ if (!HEX64.test(value))
251
+ fail("invalid-digest", `${path}.${key}`);
252
+ return value;
253
+ }
254
+ function identifierValue(record, key, guard, path) {
255
+ const value = requiredString(record, key, guard, path, REVIEW_MAX_ID_BYTES);
256
+ if (!ID.test(value))
257
+ fail("invalid-id", `${path}.${key}`);
258
+ return value;
259
+ }
260
+ function tokenValue(record, key, guard, path) {
261
+ const value = requiredString(record, key, guard, path, REVIEW_MAX_ID_BYTES);
262
+ if (!TOKEN.test(value))
263
+ fail("invalid-id", `${path}.${key}`);
264
+ return value;
265
+ }
266
+ function safePath(value, path) {
267
+ if (value.startsWith("/") ||
268
+ /^[A-Za-z]:($|\/)/.test(value) ||
269
+ value.startsWith("~") ||
270
+ value.includes("//"))
271
+ fail("invalid-path", path);
272
+ const parts = value.split("/");
273
+ if (parts.some((part) => part.length === 0 || part === "." || part === ".."))
274
+ fail("invalid-path", path);
275
+ return value;
276
+ }
277
+ function pathValue(record, key, guard, path) {
278
+ const value = requiredString(record, key, guard, path, REVIEW_MAX_PATH_BYTES, "invalid-path");
279
+ return safePath(value, `${path}.${key}`);
280
+ }
281
+ function optionalPath(record, key, guard, path) {
282
+ const value = optionalString(record, key, guard, path, REVIEW_MAX_PATH_BYTES, "invalid-path");
283
+ return value === undefined ? undefined : safePath(value, `${path}.${key}`);
284
+ }
285
+ function safeGitRef(value, path) {
286
+ if (value.length === 0 ||
287
+ value.startsWith("/") ||
288
+ value.endsWith("/") ||
289
+ value.startsWith("-") ||
290
+ value.includes("..") ||
291
+ value.includes("@{") ||
292
+ value.includes("//") ||
293
+ value.endsWith(".") ||
294
+ GIT_REF_FORBIDDEN.test(value) ||
295
+ value.includes("\u0000"))
296
+ fail("invalid-path", path);
297
+ for (const part of value.split("/")) {
298
+ if (part.length === 0 ||
299
+ part === "." ||
300
+ part === ".." ||
301
+ part.startsWith(".") ||
302
+ part.endsWith(".lock"))
303
+ fail("invalid-path", path);
304
+ }
305
+ return value;
306
+ }
307
+ function gitRefValue(record, key, guard, path) {
308
+ const value = optionalString(record, key, guard, path, REVIEW_MAX_REF_BYTES, "invalid-path");
309
+ return value === undefined ? undefined : safeGitRef(value, `${path}.${key}`);
310
+ }
311
+ function parseEvidence(value, guard, depth, path) {
312
+ const record = recordAt(value, guard, depth, path);
313
+ assertKeys(record, ["source", "token", "digest", "path"], guard, path, true);
314
+ const source = enumValue(record, "source", REVIEW_EVIDENCE_SOURCES, guard, path);
315
+ const token = tokenValue(record, "token", guard, path);
316
+ const digest = digestValue(record, "digest", guard, path);
317
+ const evidencePath = optionalPath(record, "path", guard, path);
318
+ return {
319
+ source,
320
+ token,
321
+ digest,
322
+ ...(evidencePath === undefined ? {} : { path: evidencePath }),
323
+ };
324
+ }
325
+ function parseEvidenceList(value, guard, depth, path, requireNonEmpty) {
326
+ const values = arrayAt(value, guard, depth, path);
327
+ if (requireNonEmpty && values.length === 0)
328
+ fail("missing-required", path);
329
+ const result = values.map((item, index) => parseEvidence(item, guard, depth + 1, `${path}[${index}]`));
330
+ assertSortedUnique(result, (item) => `${item.source}\u0000${item.token}\u0000${item.digest}\u0000${item.path ?? ""}`, path, "invalid-reference");
331
+ return result;
332
+ }
333
+ function parseLocation(value, guard, depth, path) {
334
+ const record = recordAt(value, guard, depth, path);
335
+ assertKeys(record, ["path", "line", "column"], guard, path);
336
+ const lineValue = optionalValue(record, "line", path);
337
+ const columnValue = optionalValue(record, "column", path);
338
+ const line = lineValue === undefined
339
+ ? undefined
340
+ : typeof lineValue === "number" && Number.isSafeInteger(lineValue) && lineValue > 0
341
+ ? lineValue
342
+ : fail("invalid-type", `${path}.line`);
343
+ const column = columnValue === undefined
344
+ ? undefined
345
+ : typeof columnValue === "number" && Number.isSafeInteger(columnValue) && columnValue > 0
346
+ ? columnValue
347
+ : fail("invalid-type", `${path}.column`);
348
+ const locationPath = pathValue(record, "path", guard, path);
349
+ return {
350
+ path: locationPath,
351
+ ...(line === undefined ? {} : { line }),
352
+ ...(column === undefined ? {} : { column }),
353
+ };
354
+ }
355
+ function parseFixRef(value, guard, depth, path) {
356
+ const record = recordAt(value, guard, depth, path);
357
+ assertKeys(record, ["recipe"], guard, path);
358
+ return { recipe: enumValue(record, "recipe", REVIEW_FIX_RECIPES, guard, path) };
359
+ }
360
+ function parseFinding(value, guard, depth, path) {
361
+ const record = recordAt(value, guard, depth, path);
362
+ assertKeys(record, ["id", "check", "code", "severity", "category", "location", "evidence", "fix"], guard, path);
363
+ const id = identifierValue(record, "id", guard, path);
364
+ const check = enumValue(record, "check", REVIEW_CHECK_IDS, guard, path);
365
+ const code = requiredString(record, "code", guard, path, REVIEW_MAX_CODE_BYTES);
366
+ if (!CODE.test(code))
367
+ fail("invalid-id", `${path}.code`);
368
+ const severity = enumValue(record, "severity", REVIEW_SEVERITIES, guard, path);
369
+ const category = enumValue(record, "category", REVIEW_CATEGORIES, guard, path);
370
+ const locationValue = optionalValue(record, "location", path);
371
+ const location = locationValue === undefined
372
+ ? undefined
373
+ : parseLocation(locationValue, guard, depth + 1, `${path}.location`);
374
+ const evidence = parseEvidenceList(requiredValue(record, "evidence", path), guard, depth + 1, `${path}.evidence`, true);
375
+ const fixValue = optionalValue(record, "fix", path);
376
+ const fix = fixValue === undefined ? undefined : parseFixRef(fixValue, guard, depth + 1, `${path}.fix`);
377
+ return {
378
+ id,
379
+ check,
380
+ code,
381
+ severity,
382
+ category,
383
+ ...(location === undefined ? {} : { location }),
384
+ evidence,
385
+ ...(fix === undefined ? {} : { fix }),
386
+ };
387
+ }
388
+ function parseCounts(value, guard, depth, path) {
389
+ const record = recordAt(value, guard, depth, path);
390
+ assertKeys(record, ["findings", "errors", "warnings", "info", "outOfScope"], guard, path);
391
+ return {
392
+ findings: integerValue(record, "findings", path),
393
+ errors: integerValue(record, "errors", path),
394
+ warnings: integerValue(record, "warnings", path),
395
+ info: integerValue(record, "info", path),
396
+ outOfScope: integerValue(record, "outOfScope", path),
397
+ };
398
+ }
399
+ function parseFindingIds(value, guard, depth, path) {
400
+ const values = arrayAt(value, guard, depth, path);
401
+ const result = values.map((item, index) => {
402
+ if (typeof item !== "string")
403
+ fail("invalid-type", `${path}[${index}]`);
404
+ guard.chargeString(item, REVIEW_MAX_ID_BYTES, "invalid-id", `${path}[${index}]`);
405
+ if (!ID.test(item))
406
+ fail("invalid-id", `${path}[${index}]`);
407
+ return item;
408
+ });
409
+ assertSortedUnique(result, (item) => item, path, "duplicate-id");
410
+ return result;
411
+ }
412
+ function parseCheck(value, guard, depth, path) {
413
+ const record = recordAt(value, guard, depth, path);
414
+ assertKeys(record, ["id", "required", "status", "duration", "counts", "findingIds", "evidence", "reasonCode"], guard, path);
415
+ const id = enumValue(record, "id", REVIEW_CHECK_IDS, guard, path);
416
+ const required = booleanValue(record, "required", path);
417
+ const status = enumValue(record, "status", REVIEW_CHECK_STATUSES, guard, path);
418
+ const duration = enumValue(record, "duration", REVIEW_DURATIONS, guard, path);
419
+ const counts = parseCounts(requiredValue(record, "counts", path), guard, depth + 1, `${path}.counts`);
420
+ const findingIds = parseFindingIds(requiredValue(record, "findingIds", path), guard, depth + 1, `${path}.findingIds`);
421
+ const evidence = parseEvidenceList(requiredValue(record, "evidence", path), guard, depth + 1, `${path}.evidence`, true);
422
+ const reasonCode = optionalEnum(record, "reasonCode", REVIEW_REASONS, guard, path);
423
+ if (status === "skipped" && (required || reasonCode !== "not-configured"))
424
+ fail("derived-field-mismatch", path);
425
+ if ((status === "unavailable" || status === "error") && reasonCode === undefined)
426
+ fail("missing-required", `${path}.reasonCode`);
427
+ if ((status === "pass" || status === "fail") && reasonCode !== undefined)
428
+ fail("derived-field-mismatch", path);
429
+ return {
430
+ id,
431
+ required,
432
+ status,
433
+ duration,
434
+ counts,
435
+ findingIds,
436
+ evidence,
437
+ ...(reasonCode === undefined ? {} : { reasonCode }),
438
+ };
439
+ }
440
+ function parsePaths(value, guard, depth, path) {
441
+ const values = arrayAt(value, guard, depth, path);
442
+ const result = values.map((item, index) => {
443
+ if (typeof item !== "string")
444
+ fail("invalid-type", `${path}[${index}]`);
445
+ guard.chargeString(item, REVIEW_MAX_PATH_BYTES, "invalid-path", `${path}[${index}]`);
446
+ return safePath(item, `${path}[${index}]`);
447
+ });
448
+ assertSortedUnique(result, (item) => item, path, "duplicate-id");
449
+ return result;
450
+ }
451
+ function parseScope(value, guard, depth, path) {
452
+ const record = recordAt(value, guard, depth, path);
453
+ assertKeys(record, ["kind", "state", "gitRef", "changedPaths", "pathDigest", "outOfScopeCount", "reasonCode"], guard, path);
454
+ const kind = enumValue(record, "kind", ["project", "diff"], guard, path);
455
+ const state = enumValue(record, "state", ["valid", "invalid"], guard, path);
456
+ const gitRef = gitRefValue(record, "gitRef", guard, path);
457
+ const changedPaths = parsePaths(requiredValue(record, "changedPaths", path), guard, depth + 1, `${path}.changedPaths`);
458
+ const pathDigest = digestValue(record, "pathDigest", guard, path);
459
+ const outOfScopeCount = integerValue(record, "outOfScopeCount", path);
460
+ const reasonCode = optionalEnum(record, "reasonCode", REVIEW_REASONS, guard, path);
461
+ if (state === "invalid" && reasonCode === undefined)
462
+ fail("missing-required", `${path}.reasonCode`);
463
+ if (state === "valid" && reasonCode !== undefined)
464
+ fail("derived-field-mismatch", path);
465
+ return {
466
+ kind,
467
+ state,
468
+ ...(gitRef === undefined ? {} : { gitRef }),
469
+ changedPaths,
470
+ pathDigest,
471
+ outOfScopeCount,
472
+ ...(reasonCode === undefined ? {} : { reasonCode }),
473
+ };
474
+ }
475
+ function parseFixResult(value, guard, depth, path) {
476
+ const record = recordAt(value, guard, depth, path);
477
+ assertKeys(record, ["recipe", "status", "changedPaths", "reasonCode"], guard, path);
478
+ const recipe = enumValue(record, "recipe", REVIEW_FIX_RECIPES, guard, path);
479
+ const status = enumValue(record, "status", REVIEW_FIX_STATUSES, guard, path);
480
+ const changedPathsValue = optionalValue(record, "changedPaths", path);
481
+ const changedPaths = changedPathsValue === undefined
482
+ ? undefined
483
+ : parsePaths(changedPathsValue, guard, depth + 1, `${path}.changedPaths`);
484
+ const reasonCode = optionalEnum(record, "reasonCode", REVIEW_REASONS, guard, path);
485
+ if ((status === "no-op" || status === "failed") && reasonCode === undefined)
486
+ fail("missing-required", `${path}.reasonCode`);
487
+ if ((status === "planned" || status === "changed") && reasonCode !== undefined)
488
+ fail("derived-field-mismatch", path);
489
+ return {
490
+ recipe,
491
+ status,
492
+ ...(changedPaths === undefined ? {} : { changedPaths }),
493
+ ...(reasonCode === undefined ? {} : { reasonCode }),
494
+ };
495
+ }
496
+ function assertSortedUnique(values, key, path, duplicateCode) {
497
+ let previous;
498
+ for (const value of values) {
499
+ const current = key(value);
500
+ if (previous !== undefined) {
501
+ if (current === previous)
502
+ fail(duplicateCode, path);
503
+ if (compareText(current, previous) < 0)
504
+ fail("non-canonical", path);
505
+ }
506
+ previous = current;
507
+ }
508
+ }
509
+ function fixKey(value) {
510
+ return `${value.recipe}\u0000${value.status}\u0000${value.changedPaths?.join("\u0000") ?? ""}\u0000${value.reasonCode ?? ""}`;
511
+ }
512
+ function assertReportRelationships(checks, findings, fixes, path) {
513
+ assertSortedUnique(checks, (item) => item.id, `${path}.checks`, "duplicate-id");
514
+ assertSortedUnique(findings, (item) => item.id, `${path}.findings`, "duplicate-id");
515
+ if (fixes !== undefined)
516
+ assertSortedUnique(fixes, fixKey, `${path}.fixes`, "duplicate-id");
517
+ const checksById = new Map();
518
+ for (const check of checks)
519
+ checksById.set(check.id, check);
520
+ const findingsById = new Map();
521
+ for (const finding of findings) {
522
+ if (findingsById.has(finding.id))
523
+ fail("duplicate-id", `${path}.findings`);
524
+ if (!checksById.has(finding.check))
525
+ fail("invalid-reference", `${path}.findings`);
526
+ findingsById.set(finding.id, finding);
527
+ }
528
+ const referenced = new Set();
529
+ let errorCount = 0;
530
+ for (const check of checks) {
531
+ let errors = 0;
532
+ let warnings = 0;
533
+ let info = 0;
534
+ for (const findingId of check.findingIds) {
535
+ const finding = findingsById.get(findingId);
536
+ if (finding === undefined || finding.check !== check.id || referenced.has(findingId))
537
+ fail("invalid-reference", `${path}.checks`);
538
+ referenced.add(findingId);
539
+ if (finding.severity === "error") {
540
+ errors += 1;
541
+ errorCount += 1;
542
+ }
543
+ else if (finding.severity === "warning") {
544
+ warnings += 1;
545
+ }
546
+ else {
547
+ info += 1;
548
+ }
549
+ }
550
+ if (check.counts.findings !== check.findingIds.length ||
551
+ check.counts.errors !== errors ||
552
+ check.counts.warnings !== warnings ||
553
+ check.counts.info !== info)
554
+ fail("derived-field-mismatch", `${path}.checks`);
555
+ }
556
+ if (referenced.size !== findings.length)
557
+ fail("invalid-reference", `${path}.findings`);
558
+ const unresolvedFixes = fixes?.filter((fix) => fix.status === "no-op" || fix.status === "failed").length ?? 0;
559
+ const blocking = errorCount + unresolvedFixes;
560
+ return { blocking, status: blocking > 0 ? "fail" : "pass", ok: blocking === 0 };
561
+ }
562
+ function deriveReportOutcome(strict, scope, checks, findings, fixes) {
563
+ let errors = 0;
564
+ let warnings = 0;
565
+ for (const finding of findings) {
566
+ if (finding.severity === "error")
567
+ errors += 1;
568
+ if (finding.severity === "warning")
569
+ warnings += 1;
570
+ }
571
+ const unresolvedFixes = fixes?.filter((fix) => fix.status === "no-op" || fix.status === "failed").length ?? 0;
572
+ const blocking = errors + (strict ? warnings : 0) + unresolvedFixes;
573
+ const inconclusive = scope.state === "invalid" ||
574
+ checks.some((check) => check.required && (check.status === "unavailable" || check.status === "error"));
575
+ const status = inconclusive ? "inconclusive" : blocking > 0 ? "fail" : "pass";
576
+ return { blocking, status, ok: status === "pass" };
577
+ }
578
+ function deepFreeze(value, seen = new WeakSet()) {
579
+ if (typeof value !== "object" || value === null)
580
+ return value;
581
+ const object = value;
582
+ if (seen.has(object))
583
+ return value;
584
+ seen.add(object);
585
+ for (const key of Reflect.ownKeys(object)) {
586
+ deepFreeze(object[key], seen);
587
+ }
588
+ return Object.freeze(value);
589
+ }
590
+ function parseObjectReport(value, guard) {
591
+ const record = recordAt(value, guard, 0, "$");
592
+ assertKeys(record, [
593
+ "version",
594
+ "strict",
595
+ "scope",
596
+ "checks",
597
+ "findings",
598
+ "fixes",
599
+ "blocking",
600
+ "ok",
601
+ "status",
602
+ "digest",
603
+ ], guard, "$");
604
+ const version = requiredValue(record, "version", "$");
605
+ if (version !== REVIEW_REPORT_VERSION)
606
+ fail("invalid-enum", "$.version");
607
+ const strict = booleanValue(record, "strict", "$");
608
+ const scope = parseScope(requiredValue(record, "scope", "$"), guard, 1, "$.scope");
609
+ const checksValue = arrayAt(requiredValue(record, "checks", "$"), guard, 1, "$.checks");
610
+ const checks = checksValue.map((item, index) => parseCheck(item, guard, 2, `$.checks[${index}]`));
611
+ const findingsValue = arrayAt(requiredValue(record, "findings", "$"), guard, 1, "$.findings");
612
+ const findings = findingsValue.map((item, index) => parseFinding(item, guard, 2, `$.findings[${index}]`));
613
+ const fixesValue = optionalValue(record, "fixes", "$");
614
+ const fixes = fixesValue === undefined
615
+ ? undefined
616
+ : arrayAt(fixesValue, guard, 1, "$.fixes").map((item, index) => parseFixResult(item, guard, 2, `$.fixes[${index}]`));
617
+ const blocking = integerValue(record, "blocking", "$");
618
+ const ok = booleanValue(record, "ok", "$");
619
+ const status = enumValue(record, "status", ["pass", "fail", "inconclusive"], guard, "$");
620
+ const digest = digestValue(record, "digest", guard, "$");
621
+ const relationshipOutcome = assertReportRelationships(checks, findings, fixes, "$");
622
+ if (relationshipOutcome.blocking < 0)
623
+ fail("derived-field-mismatch", "$.blocking");
624
+ const outcome = deriveReportOutcome(strict, scope, checks, findings, fixes);
625
+ if (blocking !== outcome.blocking || ok !== outcome.ok || status !== outcome.status)
626
+ fail("derived-field-mismatch", "$");
627
+ const report = {
628
+ version: REVIEW_REPORT_VERSION,
629
+ strict,
630
+ scope,
631
+ checks,
632
+ findings,
633
+ ...(fixes === undefined ? {} : { fixes }),
634
+ blocking,
635
+ ok,
636
+ status,
637
+ digest,
638
+ };
639
+ return report;
640
+ }
641
+ /** Parse a JSON string or unknown value into a bounded, deeply frozen review report. */
642
+ export async function parseReviewReport(input) {
643
+ let value = input;
644
+ if (typeof input === "string") {
645
+ const bytes = new TextEncoder().encode(input).byteLength;
646
+ if (bytes > REVIEW_MAX_BYTES)
647
+ fail("oversized", "$");
648
+ try {
649
+ value = JSON.parse(input);
650
+ }
651
+ catch {
652
+ fail("invalid-root", "$");
653
+ }
654
+ }
655
+ const report = parseObjectReport(value, new ParseGuard());
656
+ const canonicalBytes = new TextEncoder().encode(canonicalizeReviewReport(report)).byteLength;
657
+ if (canonicalBytes > REVIEW_MAX_BYTES)
658
+ fail("oversized", "$");
659
+ const expectedDigest = await digestReviewReport(report);
660
+ if (report.digest !== expectedDigest)
661
+ fail("derived-field-mismatch", "$.digest");
662
+ return deepFreeze(report);
663
+ }
664
+ //# sourceMappingURL=parser.js.map