@action-state-group/capsule-viewer 0.0.0-stage → 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +201 -0
  2. package/LICENSE-BSD-3-Clause +28 -0
  3. package/README.md +24 -2
  4. package/dist/browser.d.ts +4 -0
  5. package/dist/browser.js +11 -0
  6. package/dist/browser.js.map +1 -0
  7. package/dist/compliance-presentation.d.ts +51 -0
  8. package/dist/compliance-presentation.js +140 -0
  9. package/dist/compliance-presentation.js.map +1 -0
  10. package/dist/compliance-styles.d.ts +8 -0
  11. package/dist/compliance-styles.js +66 -0
  12. package/dist/compliance-styles.js.map +1 -0
  13. package/dist/compliance-view.d.ts +6 -0
  14. package/dist/compliance-view.js +394 -0
  15. package/dist/compliance-view.js.map +1 -0
  16. package/dist/compliance.d.ts +92 -0
  17. package/dist/compliance.js +333 -0
  18. package/dist/compliance.js.map +1 -0
  19. package/dist/composed-view.d.ts +24 -0
  20. package/dist/composed-view.js +179 -0
  21. package/dist/composed-view.js.map +1 -0
  22. package/dist/emitter-shell.html +136 -0
  23. package/dist/emitter.d.ts +69 -0
  24. package/dist/emitter.js +266 -0
  25. package/dist/emitter.js.map +1 -0
  26. package/dist/evidence-graph-view.d.ts +96 -0
  27. package/dist/evidence-graph-view.js +1480 -0
  28. package/dist/evidence-graph-view.js.map +1 -0
  29. package/dist/evidence-graph.iife.js +11373 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +7 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/outcome-report-styles.d.ts +16 -0
  34. package/dist/outcome-report-styles.js +225 -0
  35. package/dist/outcome-report-styles.js.map +1 -0
  36. package/dist/outcome-report-view.d.ts +5 -0
  37. package/dist/outcome-report-view.js +885 -0
  38. package/dist/outcome-report-view.js.map +1 -0
  39. package/dist/presentation-builder.d.ts +247 -0
  40. package/dist/presentation-builder.js +552 -0
  41. package/dist/presentation-builder.js.map +1 -0
  42. package/dist/presentation-mount.d.ts +26 -0
  43. package/dist/presentation-mount.js +45 -0
  44. package/dist/presentation-mount.js.map +1 -0
  45. package/package.json +53 -4
@@ -0,0 +1,1480 @@
1
+ // SPDX-License-Identifier: BSD-3-Clause
2
+ import { buildVerifiedBundleContext, disclosureOf, isVerifiedBundleContext, withCountersigners, } from "@action-state-group/agent-action-capsule";
3
+ import { BUILTIN_MANIFEST_COMPOSED, BUILTIN_MANIFEST_EVALUATION_SUMMARY_GRAPH, BUILTIN_MANIFEST_NO_AGGREGATE, BUILTIN_MANIFEST_REPORT_ROWS, BUILTIN_MANIFEST_RESULT, BUILTIN_MANIFEST_RESULT_COMPLIANCE, BUILTIN_MANIFEST_RESULT_OUTCOME_REPORT, } from "@action-state-group/agent-action-capsule";
4
+ import { classifyCountersignatures, declaredProducerKeys, } from "@action-state-group/agent-action-capsule";
5
+ import { buildEvidenceGraph, EvidenceGraphError, zoneStatement, } from "@action-state-group/agent-action-capsule";
6
+ import { digestCapability } from "@action-state-group/agent-action-capsule";
7
+ import { renderOutcomeReportPage } from "./outcome-report-view.js";
8
+ import { readOutcomeReportPresentation, } from "@action-state-group/agent-action-capsule";
9
+ import { renderCompliancePage } from "./compliance-view.js";
10
+ import { composedSectionModule } from "./composed-view.js";
11
+ import { readCompliancePresentation, } from "./compliance-presentation.js";
12
+ import { readPresentationBlock } from "@action-state-group/agent-action-capsule";
13
+ import { bundleVerified, PresentationAmbiguityError, PresentationRegistry, } from "@action-state-group/agent-action-capsule";
14
+ import { buildReportRows, } from "@action-state-group/agent-action-capsule";
15
+ import { buildResultRoot, isResultRoot, UNVERIFIED_KEY_LABEL, } from "@action-state-group/agent-action-capsule";
16
+ import { buildVerificationPageModel, CHECKED_ON_THIS_PAGE, CHECKS_SCOPE_LINE, FAILED_ON_THIS_PAGE, FULL_VERIFIER_LINE, NOT_CHECKED_ON_THIS_PAGE, RECEIPT_GRADE_DISPLAY, verificationCheckWords, unboundRecordIds, } from "@action-state-group/agent-action-capsule";
17
+ function element(tag, text) {
18
+ const value = document.createElement(tag);
19
+ if (text !== undefined)
20
+ value.textContent = text;
21
+ return value;
22
+ }
23
+ function display(value) {
24
+ try {
25
+ return JSON.stringify(value);
26
+ }
27
+ catch {
28
+ return String(value);
29
+ }
30
+ }
31
+ function appendValue(parent, label, value) {
32
+ parent.append(element("dt", label));
33
+ parent.append(element("dd", display(value)));
34
+ }
35
+ // A written time is printed exactly as the source wrote it. When it states no
36
+ // zone, a visible marker follows it; no zone is ever assigned and no `Z` or
37
+ // UTC label is ever printed on a time that did not carry one.
38
+ function renderTime(value) {
39
+ const zone = zoneStatement(value);
40
+ const time = element("span", value);
41
+ time.dataset.tz = zone;
42
+ if (zone === "not-stated") {
43
+ const marker = element("span", " (timezone not stated)");
44
+ marker.dataset.tzMarker = "not-stated";
45
+ time.append(marker);
46
+ }
47
+ return time;
48
+ }
49
+ function appendTime(parent, label, value, absent) {
50
+ parent.append(element("dt", label));
51
+ const cell = element("dd");
52
+ if (value === undefined) {
53
+ cell.textContent = absent;
54
+ cell.dataset.time = "not-stated";
55
+ }
56
+ else {
57
+ cell.append(renderTime(value));
58
+ }
59
+ parent.append(cell);
60
+ }
61
+ const recordsWord = (count) => `${count} ${count === 1 ? "record" : "records"}`;
62
+ // The banner is drawn from the verification result alone and precedes every
63
+ // row in the DOM; an unverified bundle gets the refusal line here and no
64
+ // rows at all, so a reader never meets a payload before the verdict on the
65
+ // bundle that carries it. It says what IS proven: a verified bundle with
66
+ // records outside the checkpoint names their count up front.
67
+ function renderVerificationBanner(root, verified, coverage) {
68
+ const banner = element("p", verified
69
+ ? coverage.uncheckpointed === 0
70
+ ? "Bundle verification passed"
71
+ : `Bundle verification passed; ${coverage.uncheckpointed} of ${recordsWord(coverage.total)} uncheckpointed`
72
+ : "Bundle verification failed");
73
+ banner.dataset.verify = verified ? "verified" : "failed";
74
+ banner.dataset.uncheckpointed = String(coverage.uncheckpointed);
75
+ root.append(banner);
76
+ if (verified)
77
+ return banner;
78
+ const refusal = element("p", "This bundle did not verify. Its records, rows and payloads are not shown; the verification page below lists which checks failed.");
79
+ refusal.dataset.refusal = "unverified-bundle";
80
+ root.append(refusal);
81
+ return banner;
82
+ }
83
+ /** The words a page says when this viewer cannot verify the bundle at all. */
84
+ export const UNABLE_TO_VERIFY_LINES = {
85
+ banner: "Unable to verify in this viewer",
86
+ reason: "This viewer cannot compute the SHA-256 digests the checks need, so nothing was checked and no records are shown. This is not a failed check: the file may well be valid.",
87
+ command: "To check this file, run:",
88
+ noCommand: "To check this file, open it from a secure (https) page, or check it with a verifier outside this viewer.",
89
+ };
90
+ /**
91
+ * Render the "unable to verify in this viewer" state into `root`: drawn,
92
+ * in place of any verification result, when this runtime has no SHA-256
93
+ * (`digestCapability() === "none"`). It never says the bundle failed, and
94
+ * it shows no record. `verifyCommand` is the exact command a reader runs
95
+ * to check the file elsewhere; without one, the page says where to check
96
+ * it instead.
97
+ */
98
+ export function renderUnableToVerify(root, options = {}) {
99
+ root.replaceChildren();
100
+ const banner = element("p", UNABLE_TO_VERIFY_LINES.banner);
101
+ banner.dataset.verify = "unable";
102
+ const reason = element("p", UNABLE_TO_VERIFY_LINES.reason);
103
+ reason.dataset.refusal = "unable-to-verify";
104
+ root.append(banner, reason);
105
+ if (options.verifyCommand === undefined) {
106
+ root.append(element("p", UNABLE_TO_VERIFY_LINES.noCommand));
107
+ return;
108
+ }
109
+ const command = element("code", options.verifyCommand);
110
+ command.dataset.verifyCommand = "";
111
+ const block = element("pre");
112
+ block.append(command);
113
+ root.append(element("p", UNABLE_TO_VERIFY_LINES.command), block);
114
+ }
115
+ // Drawn in the outcome-report card's own look when that card renders
116
+ // (OUTCOME_REPORT_CSS's .oi-banner rules); the words and data attributes are
117
+ // identical either way -- the class is presentation only.
118
+ function styleBanner(banner, verified) {
119
+ banner.className = `oi oi-banner ${verified ? "oi-banner-ok" : "oi-banner-failed"}`;
120
+ }
121
+ // presentation/v1 is rendered here, in the header only, and nowhere else in
122
+ // this module. Only readPresentationBlock's three known fields ever reach
123
+ // the DOM -- the chrome rule holds structurally, not by a rendering-site
124
+ // convention that a future edit could bypass.
125
+ function renderPresentationHeader(root, bundle) {
126
+ const presentation = readPresentationBlock(bundle);
127
+ if (presentation === undefined)
128
+ return;
129
+ const header = element("header");
130
+ header.dataset.presentation = "header";
131
+ if (presentation.logoDataUrl !== undefined) {
132
+ const logo = document.createElement("img");
133
+ logo.src = presentation.logoDataUrl;
134
+ logo.alt = presentation.producerDisplayName ?? "producer logo";
135
+ header.append(logo);
136
+ }
137
+ if (presentation.producerDisplayName !== undefined) {
138
+ const name = element("span", presentation.producerDisplayName);
139
+ name.dataset.presentationField = "producer-display-name";
140
+ header.append(name);
141
+ }
142
+ if (presentation.title !== undefined) {
143
+ const title = element("strong", presentation.title);
144
+ title.dataset.presentationField = "title";
145
+ header.append(title);
146
+ }
147
+ root.append(header);
148
+ }
149
+ function stampText(stamp) {
150
+ switch (stamp.kind) {
151
+ case "hollow":
152
+ return "Countersigned: none";
153
+ case "unverified":
154
+ return stamp.reason === "no-ed25519"
155
+ ? `a ${stamp.type} countersignature is present; its signature is not checked in this viewer`
156
+ : `a ${stamp.type} countersignature is present; this viewer does not verify that type`;
157
+ case "invalid":
158
+ return "a countersignature is present but failed to verify";
159
+ case "not-independent":
160
+ return `countersigned by the producer — not independent · recomputed ${stamp.statement.recomputedAt}`;
161
+ case "unresolved-signer":
162
+ return `countersigned by an unlisted signer, not in any countersigner list consulted · recomputed ${stamp.statement.recomputedAt}`;
163
+ case "resolved":
164
+ return `Countersigned by ${stamp.name} · recomputed ${stamp.statement.recomputedAt}`;
165
+ }
166
+ }
167
+ // Each check and result is the signer's statement, listed as the signer gave
168
+ // it: never totalled, never presented as this viewer's own finding.
169
+ function renderSignerStatement(item, signer, statement) {
170
+ const label = element("p", `${signer}'s statement of what it recomputed:`);
171
+ item.append(label);
172
+ const checks = element("ul");
173
+ checks.dataset.countersignStatement = "checks";
174
+ statement.checks.forEach((check) => {
175
+ const row = element("li", `${check.name}: ${check.result}`);
176
+ row.dataset.checkResult = check.result;
177
+ checks.append(row);
178
+ });
179
+ item.append(checks);
180
+ if (statement.receipt === "unverified") {
181
+ const receipt = element("p", "receipt present, not checked by this viewer");
182
+ receipt.dataset.countersignReceipt = "unverified";
183
+ item.append(receipt);
184
+ }
185
+ }
186
+ function renderStamps(host, stamps) {
187
+ host.append(element("h4", "Countersignatures"));
188
+ const list = element("ul");
189
+ stamps.forEach((stamp) => {
190
+ const item = element("li");
191
+ item.dataset.stampKind = stamp.kind;
192
+ item.append(element("span", stampText(stamp)));
193
+ if (stamp.kind === "resolved") {
194
+ renderSignerStatement(item, stamp.name, stamp.statement);
195
+ }
196
+ else if (stamp.kind === "not-independent") {
197
+ renderSignerStatement(item, "The producer", stamp.statement);
198
+ }
199
+ else if (stamp.kind === "unresolved-signer") {
200
+ renderSignerStatement(item, "The unlisted signer", stamp.statement);
201
+ }
202
+ list.append(item);
203
+ });
204
+ host.append(list);
205
+ }
206
+ function renderReceipts(host, receipts) {
207
+ host.append(element("h4", "Receipts"));
208
+ if (receipts.length === 0) {
209
+ host.append(element("p", "no receipts disclosed"));
210
+ return;
211
+ }
212
+ const list = element("ul");
213
+ receipts.forEach((receipt) => {
214
+ const item = element("li", `${receipt.witness} · ${RECEIPT_GRADE_DISPLAY[receipt.grade]} · `);
215
+ item.append(renderTime(receipt.time));
216
+ list.append(item);
217
+ });
218
+ host.append(list);
219
+ }
220
+ const COVERAGE_LABEL = Object.freeze({
221
+ checkpointed: "checkpointed",
222
+ uncheckpointed: "uncheckpointed",
223
+ membership_invalid: "membership invalid",
224
+ unverified: "membership unverified",
225
+ });
226
+ // One row per supplied record, in bundle order, each with its own standing
227
+ // under the checkpoint. When the claim established coverage the count is
228
+ // stated in words above the list. When it did not, the line says so and
229
+ // names the verifier's reason -- never "0 records uncheckpointed" on a
230
+ // bundle whose memberships were never verified -- and when the claim was
231
+ // withheld (no checkpoint to stand under) there is no list at all.
232
+ function renderCheckpointCoverage(host, records, uncheckpointed, coverage) {
233
+ host.append(element("h4", "Checkpoint coverage"));
234
+ if (coverage.status === "established") {
235
+ const count = element("p", `${recordsWord(uncheckpointed)} uncheckpointed of ${recordsWord(records.length)} supplied`);
236
+ count.dataset.coverage = "uncheckpointed";
237
+ count.dataset.count = String(uncheckpointed);
238
+ host.append(count);
239
+ }
240
+ else {
241
+ const line = element("p", `coverage not established: ${coverage.reason}`);
242
+ line.dataset.coverage = "not-established";
243
+ line.dataset.claim = coverage.status;
244
+ host.append(line);
245
+ if (coverage.status === "withheld")
246
+ return;
247
+ }
248
+ host.append(renderCoverageStatusCounts(records));
249
+ const details = element("details");
250
+ details.dataset.records = "coverage-detail";
251
+ const summary = element("summary", `${recordsWord(records.length)}, by capsule id`);
252
+ details.append(summary);
253
+ const list = element("ul");
254
+ list.dataset.records = "coverage";
255
+ for (const record of records) {
256
+ const item = element("li", `${record.capsuleId} · `);
257
+ const status = element("span", COVERAGE_LABEL[record.status]);
258
+ status.dataset.recordStatus = record.status;
259
+ status.className = `seal-${record.status}`;
260
+ item.dataset.capsuleId = record.capsuleId;
261
+ item.append(status);
262
+ list.append(item);
263
+ }
264
+ details.append(list);
265
+ host.append(details);
266
+ }
267
+ // Counts by seal status -- the line a reader actually needs (how many of
268
+ // each) without scrolling a list that is one row per record (500+ rows on a
269
+ // real day's book: 50 cases x 9 criteria + 50 cases + 1 Close). The full,
270
+ // per-capsule-id list stays available (renderCheckpointCoverage wraps it in
271
+ // a collapsed <details>), never removed, just not the first thing rendered.
272
+ function renderCoverageStatusCounts(records) {
273
+ const counts = new Map();
274
+ for (const record of records) {
275
+ counts.set(record.status, (counts.get(record.status) ?? 0) + 1);
276
+ }
277
+ const list = element("ul");
278
+ list.dataset.records = "coverage-summary";
279
+ for (const status of Object.keys(COVERAGE_LABEL)) {
280
+ const count = counts.get(status) ?? 0;
281
+ if (count === 0)
282
+ continue;
283
+ const item = element("li", `${recordsWord(count)} ${COVERAGE_LABEL[status]}`);
284
+ item.dataset.coverageStatus = status;
285
+ item.dataset.count = String(count);
286
+ list.append(item);
287
+ }
288
+ return list;
289
+ }
290
+ function renderCompletenessStatement(host, completeness) {
291
+ host.append(element("h4", "Completeness statement"));
292
+ const details = element("dl");
293
+ appendValue(details, "closure depth", completeness?.closureDepth ?? "unstated");
294
+ appendValue(details, "records mode", completeness?.recordsMode ?? "unstated");
295
+ appendValue(details, "payloads mode", completeness?.payloadsMode ?? "unstated");
296
+ appendValue(details, "suppressed fields", completeness?.suppressedFields ?? []);
297
+ host.append(details);
298
+ }
299
+ function renderChecks(host, checks) {
300
+ host.append(element("h4", "The ten checks"));
301
+ const list = element("ol");
302
+ checks.forEach((check) => {
303
+ const item = element("li");
304
+ item.dataset.checkStatus = check.status;
305
+ item.append(element("strong", check.name));
306
+ item.append(element("span", `: ${check.result}`));
307
+ list.append(item);
308
+ });
309
+ host.append(list);
310
+ }
311
+ // One row per extension: id, integrity, semantics. The id is the bundle's
312
+ // own member name, set as text only. Integrity and semantics are separate
313
+ // cells so "covered" can never read as "understood": an uninterpreted block
314
+ // says so in words (EXTENSION_NOT_INTERPRETED). A row whose meaning a refused
315
+ // presentation module would have applied says that instead (presentation
316
+ // contract section 3.2), and names the module and the reason. A refused
317
+ // module with no extension of its own gets one line after the table. Nothing
318
+ // is drawn for a bundle with no extensions and no refusal.
319
+ function renderExtensions(host, rows, refusals) {
320
+ renderExtensionTable(host, rows);
321
+ for (const notice of refusals) {
322
+ const line = element("p", notice.line);
323
+ line.dataset.presentationRefused = notice.id;
324
+ line.dataset.refusal = notice.reason;
325
+ host.append(line);
326
+ }
327
+ }
328
+ function renderExtensionTable(host, rows) {
329
+ if (rows.length === 0)
330
+ return;
331
+ host.append(element("h4", "Extensions"));
332
+ const table = element("table");
333
+ table.dataset.extensions = "";
334
+ const head = element("tr");
335
+ for (const label of ["extension", "integrity", "semantics"])
336
+ head.append(element("th", label));
337
+ const thead = element("thead");
338
+ thead.append(head);
339
+ const body = element("tbody");
340
+ for (const row of rows) {
341
+ const tr = element("tr");
342
+ tr.dataset.extensionId = row.id;
343
+ tr.dataset.integrity = row.integrity;
344
+ tr.dataset.semantics =
345
+ row.refusal !== undefined
346
+ ? "refused"
347
+ : row.interpreter === undefined
348
+ ? "uninterpreted"
349
+ : "interpreted";
350
+ if (row.refusal !== undefined) {
351
+ tr.dataset.refusedModule = row.refusal.id;
352
+ tr.dataset.refusal = row.refusal.reason;
353
+ }
354
+ tr.append(element("td", row.id), element("td", row.integrity), element("td", row.semantics));
355
+ body.append(tr);
356
+ }
357
+ table.append(thead, body);
358
+ host.append(table);
359
+ }
360
+ // The page's own checks in up to three lists (presentation contract section
361
+ // 6.1): what ran here and passed, what ran here and failed, and what the
362
+ // bundle calls for that this page did not run. A list with no entries is not
363
+ // drawn. The not-checked list ends with the neutral pointer to a full
364
+ // verifier and, when the host supplied one, its own verifier text (shown as
365
+ // text, never markup).
366
+ function renderCheckLists(host, lists, verifierHint) {
367
+ const groups = [
368
+ ["page", CHECKED_ON_THIS_PAGE, lists.page],
369
+ ["failed", FAILED_ON_THIS_PAGE, lists.failed],
370
+ ["not-checked", NOT_CHECKED_ON_THIS_PAGE, lists.notChecked],
371
+ ];
372
+ for (const [which, heading, ids] of groups) {
373
+ if (ids.length === 0)
374
+ continue;
375
+ host.append(element("h4", heading));
376
+ const list = element("ul");
377
+ list.dataset.checks = which;
378
+ for (const id of ids) {
379
+ const item = element("li", verificationCheckWords(id, which === "not-checked" ? "notChecked" : which, lists));
380
+ item.dataset.check = id;
381
+ list.append(item);
382
+ }
383
+ host.append(list);
384
+ if (which === "not-checked") {
385
+ const pointer = element("p", FULL_VERIFIER_LINE);
386
+ pointer.dataset.checks = "full-verifier";
387
+ host.append(pointer);
388
+ if (verifierHint !== undefined && verifierHint !== "") {
389
+ const hint = element("pre", verifierHint);
390
+ hint.dataset.verifierHint = "";
391
+ host.append(hint);
392
+ }
393
+ }
394
+ }
395
+ const scope = element("p", CHECKS_SCOPE_LINE);
396
+ scope.dataset.checks = "scope";
397
+ host.append(scope);
398
+ }
399
+ // The viewer-owned verification page: the last page of the rendering, drawn
400
+ // entirely from VERIFIED data (the already-computed BundleVerificationResult
401
+ // and the countersignature stamp classification), never from bundle-supplied
402
+ // markup. It is never labeled a certificate.
403
+ //
404
+ // `styled` (true only when the outcome-report card rendered above it) draws
405
+ // this same page in that card's look: the section takes the card's `.oi`
406
+ // scope plus `.oi-vp`, and its content goes inside one `.sec` panel like
407
+ // every card section. Content, order and data attributes are identical
408
+ // either way, and it stays the last element of the rendering.
409
+ //
410
+ // `applied` is the set of extension interpreters this rendering ran; an
411
+ // extension row says "interpreted by" only for one of them. `refused` is
412
+ // every module this runtime refused that matched (page, then section).
413
+ async function renderVerificationPage(root, context, styled, applied, refused, citedSigners, verifierHint) {
414
+ const { bundle, verification: verified, countersigners } = context;
415
+ const section = element("section");
416
+ section.dataset.page = "verification";
417
+ let page = section;
418
+ if (styled) {
419
+ section.className = "oi oi-vp";
420
+ page = element("div");
421
+ page.className = "sec";
422
+ section.append(page);
423
+ }
424
+ page.append(element("h2", "Verification"));
425
+ const stamps = await classifyCountersignatures(context.countersignatures.map((entry) => entry.value), verified.bundleDigest, declaredProducerKeys(bundle), countersigners);
426
+ const model = buildVerificationPageModel(bundle, verified, applied, refused, {
427
+ stamps,
428
+ citedSigners,
429
+ });
430
+ const summary = element("dl");
431
+ appendValue(summary, "bundle digest", model.bundleDigest ?? "uncomputable");
432
+ appendValue(summary, "checkpoint root", model.checkpointRoot ?? "absent");
433
+ appendValue(summary, "checkpoint size", model.checkpointSize ?? "absent");
434
+ page.append(summary);
435
+ renderReceipts(page, model.receipts);
436
+ if (model.selfWitnessed) {
437
+ // A checkpoint with no transparency-service receipt was witnessed by
438
+ // nobody but the log that produced it -- stated here in those words.
439
+ const witness = element("p", "self-witnessed: no transparency-service receipt");
440
+ witness.dataset.witness = "self";
441
+ page.append(witness);
442
+ }
443
+ renderCheckpointCoverage(page, model.records, model.uncheckpointedCount, model.coverage);
444
+ renderStamps(page, stamps);
445
+ renderCompletenessStatement(page, model.completeness);
446
+ renderChecks(page, model.checks);
447
+ renderExtensions(page, model.extensions, model.presentationRefusals);
448
+ renderCheckLists(page, model.checkLists, verifierHint);
449
+ page.append(element("p", model.verifyIndependentlyLine));
450
+ root.append(section);
451
+ }
452
+ function metRate(report) {
453
+ const required = report.outcomes.filter((outcome) => outcome.role === "required");
454
+ if (required.length === 0)
455
+ return "met rate unavailable";
456
+ const passed = required.filter((outcome) => outcome.aggregate === "pass").length;
457
+ return `met rate ${passed}/${required.length}`;
458
+ }
459
+ function renderCalibration(calibration) {
460
+ const section = element("section");
461
+ section.append(element("h2", "Human check"));
462
+ if (calibration === undefined) {
463
+ section.append(element("p", "no human-check data"));
464
+ return section;
465
+ }
466
+ const details = element("dl");
467
+ appendValue(details, "confusion matrix", calibration.confusion);
468
+ appendCount(details, "agreement", calibration.agreement);
469
+ appendCount(details, "corrected rate", calibration.correctedRate);
470
+ appendValue(details, "period window", calibration.periodWindow);
471
+ section.append(details);
472
+ return section;
473
+ }
474
+ // A calibration figure is printed as the integers it is made of, "k of n";
475
+ // no rate is computed from them here, and a figure the producer stated only
476
+ // as a rate is said to be that, not converted.
477
+ function appendCount(parent, label, count) {
478
+ parent.append(element("dt", label));
479
+ const cell = element("dd");
480
+ if (count === undefined) {
481
+ cell.textContent = "not stated";
482
+ cell.dataset.count = "not-stated";
483
+ }
484
+ else if ("rate" in count) {
485
+ cell.textContent = "rate given, k and n not stated";
486
+ cell.dataset.count = "not-stated";
487
+ }
488
+ else {
489
+ cell.textContent = `${count.k} of ${count.n}`;
490
+ cell.dataset.k = String(count.k);
491
+ cell.dataset.n = String(count.n);
492
+ }
493
+ parent.append(cell);
494
+ }
495
+ // Both of a record's times are shown, each labelled and each as written. The
496
+ // action time is the source's own; when the record states none it says so --
497
+ // the seal time (the capsule's registration timestamp) never stands in for it.
498
+ function renderProvenance(capsuleId, coordinates, times) {
499
+ const panel = element("section");
500
+ panel.append(element("h4", "Provenance"));
501
+ const details = element("dl");
502
+ appendValue(details, "capsule ID", capsuleId);
503
+ if (times.provenanceMode !== undefined)
504
+ appendValue(details, "provenance", times.provenanceMode);
505
+ appendTime(details, "action time", times.actionTime, "action time not stated");
506
+ appendTime(details, "seal time", times.sealTime, "seal time not stated");
507
+ // The seal status is this record's own: coordinates exist only for a
508
+ // record whose inclusion proof verified (the bundle renders only then), so
509
+ // a record without them sits outside the checkpoint and says so.
510
+ details.append(element("dt", "seal status"));
511
+ const seal = element("dd", coordinates === undefined ? "uncheckpointed" : "checkpointed");
512
+ seal.dataset.seal =
513
+ coordinates === undefined ? "uncheckpointed" : "checkpointed";
514
+ seal.className = `seal-${seal.dataset.seal}`;
515
+ details.append(seal);
516
+ if (coordinates !== undefined) {
517
+ appendValue(details, "log ID", coordinates.logId);
518
+ appendValue(details, "sequence", coordinates.seq);
519
+ appendValue(details, "leaf", coordinates.leafIndex);
520
+ }
521
+ panel.append(details);
522
+ return panel;
523
+ }
524
+ function renderJudgment(judgment) {
525
+ const item = element("li");
526
+ item.append(element("strong", `${judgment.axisId}: ${judgment.status}`));
527
+ item.append(element("p", judgment.rationale));
528
+ item.append(element("p", `evidence: ${judgment.evidenceIds.join(", ")}`));
529
+ return item;
530
+ }
531
+ function renderAct(act) {
532
+ const section = element("section");
533
+ section.append(element("h4", `Turn ${act.turnIdx}`));
534
+ if (act.agentInput !== undefined)
535
+ section.append(element("pre", display(act.agentInput)));
536
+ if (act.agentOutput !== undefined)
537
+ section.append(element("pre", display(act.agentOutput)));
538
+ section.append(renderProvenance(act.capsuleId, act.logCoordinates, act));
539
+ return section;
540
+ }
541
+ function object(value) {
542
+ return value !== null && typeof value === "object" && !Array.isArray(value)
543
+ ? value
544
+ : {};
545
+ }
546
+ function renderWithheldActs(acts, host) {
547
+ for (const act of acts) {
548
+ if (act.agentInput !== undefined && act.agentOutput !== undefined)
549
+ continue;
550
+ const committed = object({
551
+ agent_input_digest: act.agentInputDigest,
552
+ agent_output_digest: act.agentOutputDigest,
553
+ });
554
+ const evidence = element("section");
555
+ evidence.append(element("h4", "Undisclosed payload evidence"));
556
+ const details = element("dl");
557
+ appendValue(details, "capsule ID", act.capsuleId);
558
+ for (const field of ["agent_input_digest", "agent_output_digest"]) {
559
+ if (typeof committed[field] === "string")
560
+ appendValue(details, field, committed[field]);
561
+ }
562
+ evidence.append(details);
563
+ // A supplied value that does not hash to the committed digest is never
564
+ // shown; the reader sees the digest above and this note, nothing else.
565
+ for (const [field, state] of [
566
+ ["agent_input", act.agentInputDisclosure],
567
+ ["agent_output", act.agentOutputDisclosure],
568
+ ]) {
569
+ if (state !== "disclosure_mismatch")
570
+ continue;
571
+ const note = element("p", `${field}: the disclosed value does not match the committed digest and is withheld`);
572
+ note.dataset.disclosure = state;
573
+ evidence.append(note);
574
+ }
575
+ host.append(evidence);
576
+ }
577
+ }
578
+ function renderCase(caseNode, host) {
579
+ host.replaceChildren();
580
+ host.append(element("h3", `Case ${caseNode.caseId}`));
581
+ const judgments = element("ul");
582
+ caseNode.judgments.forEach((judgment) => judgments.append(renderJudgment(judgment)));
583
+ host.append(element("h4", "Axis judgments"), judgments);
584
+ host.append(element("h4", "Disclosed transcript"));
585
+ caseNode.acts.forEach((act) => host.append(renderAct(act)));
586
+ renderWithheldActs(caseNode.acts, host);
587
+ }
588
+ function renderReport(report, host, _records) {
589
+ host.replaceChildren();
590
+ const heading = element("h2", "Cases for ");
591
+ heading.append(renderTime(report.date));
592
+ host.append(heading);
593
+ const outcomes = element("ul");
594
+ report.outcomes.forEach((outcome) => {
595
+ outcomes.append(element("li", `${outcome.outcomeId} (${outcome.role}): ${outcome.aggregate ?? "unrated"}`));
596
+ });
597
+ host.append(element("h3", "Outcome rollup"), outcomes);
598
+ renderWithheldActs(report.withheldActs, host);
599
+ const cases = element("section");
600
+ const detail = element("section");
601
+ report.cases.forEach((caseNode) => {
602
+ const item = element("button", `${caseNode.taskId}, trial ${caseNode.trial}: ${caseNode.aggregate ?? "unrated"}`);
603
+ item.setAttribute("type", "button");
604
+ item.dataset.caseId = caseNode.caseId;
605
+ item.addEventListener("click", () => renderCase(caseNode, detail));
606
+ cases.append(item);
607
+ });
608
+ host.append(cases, detail, renderProvenance(report.capsuleId, report.logCoordinates, report));
609
+ }
610
+ // report/v1: the generic row path. Every row is rendered from its own data
611
+ // -- label, status, reason, citations -- never from report-specific markup,
612
+ // so a pack this module has never heard of (obligations, Consumer Duty,
613
+ // anything else) renders the same way an outcomes report does.
614
+ function renderCitation(citation) {
615
+ const section = element("section");
616
+ section.append(renderProvenance(citation.capsuleId, citation.logCoordinates, citation));
617
+ if (citation.disclosure === "disclosed") {
618
+ section.append(element("pre", display(citation.disclosedPayload)));
619
+ }
620
+ else {
621
+ const note = element("p", citation.disclosure === "disclosure_mismatch"
622
+ ? "withheld: the disclosed value does not match the committed digest"
623
+ : "withheld");
624
+ note.dataset.disclosure = citation.disclosure;
625
+ section.append(note);
626
+ }
627
+ return section;
628
+ }
629
+ function renderReportRow(row, host) {
630
+ host.replaceChildren();
631
+ host.append(element("h3", row.label));
632
+ const status = element("p", row.status.replaceAll("_", " "));
633
+ status.dataset.rowStatus = row.status;
634
+ host.append(status);
635
+ if (row.reason !== undefined)
636
+ host.append(element("p", row.reason));
637
+ host.append(element("h4", "Evidence"));
638
+ if (row.citations.length === 0) {
639
+ host.append(element("p", "no citation"));
640
+ }
641
+ else {
642
+ row.citations.forEach((citation) => host.append(renderCitation(citation)));
643
+ }
644
+ }
645
+ function renderReportRowsTable(reportRows, root) {
646
+ const section = element("section");
647
+ section.dataset.page = "report-rows";
648
+ section.append(element("h1", reportRows.title ?? "Report"));
649
+ const table = document.createElement("table");
650
+ const detail = element("section");
651
+ reportRows.rows.forEach((row) => {
652
+ const tr = document.createElement("tr");
653
+ const labelCell = document.createElement("td");
654
+ const button = element("button", row.label);
655
+ button.setAttribute("type", "button");
656
+ button.dataset.rowId = row.rowId;
657
+ button.addEventListener("click", () => renderReportRow(row, detail));
658
+ labelCell.append(button);
659
+ const statusCell = element("td", row.status.replaceAll("_", " "));
660
+ statusCell.dataset.rowStatus = row.status;
661
+ tr.append(labelCell, statusCell);
662
+ table.append(tr);
663
+ });
664
+ section.append(table, detail, renderProvenance(reportRows.capsuleId, reportRows.logCoordinates, reportRows));
665
+ root.append(section);
666
+ }
667
+ // Result v0 root: headlines from the root Result, drill-downs from the
668
+ // records it cites. A cited record is drawn from its own data -- provenance,
669
+ // each committed member as disclosed or as `withheld · <digest>`, and the
670
+ // records it cites in turn -- so aggregate → daily → case → act is a walk
671
+ // over citations, never report-specific markup.
672
+ function renderCitedMember(host, field, resolution, committed) {
673
+ if (resolution.state !== "disclosed" && committed === undefined)
674
+ return;
675
+ host.append(element("h5", field));
676
+ if (resolution.state === "disclosed") {
677
+ host.append(element("pre", display(resolution.payload)));
678
+ return;
679
+ }
680
+ // A withheld member shows the digest the record committed to; a supplied
681
+ // value that does not hash to it is never shown, only noted.
682
+ const note = element("p", resolution.state === "disclosure_mismatch"
683
+ ? `withheld · ${committed}: the disclosed value does not match the committed digest`
684
+ : `withheld · ${committed}`);
685
+ note.dataset.disclosure = resolution.state;
686
+ host.append(note);
687
+ }
688
+ function renderCitedRecord(record, records, host) {
689
+ const section = element("section");
690
+ section.dataset.citedRecord = record.capsuleId;
691
+ section.append(renderProvenance(record.capsuleId, record.logCoordinates, record));
692
+ renderCitedMember(section, "agent_input", record.agentInput, record.agentInputDigest);
693
+ renderCitedMember(section, "agent_output", record.agentOutput, record.agentOutputDigest);
694
+ if (record.cites.length > 0) {
695
+ section.append(element("h5", "Cites"));
696
+ renderCitationList(record.cites, records, section);
697
+ }
698
+ host.append(section);
699
+ }
700
+ // One row per cited id, in citation order: a record supplied in this bundle
701
+ // opens under the list on click; one that is not says so and stays.
702
+ function renderCitationList(ids, records, host) {
703
+ const list = element("ul");
704
+ const detail = element("section");
705
+ for (const id of ids) {
706
+ const item = element("li");
707
+ const target = records.get(id);
708
+ if (target === undefined) {
709
+ item.textContent = `${id} · not in this bundle`;
710
+ item.dataset.citation = "missing";
711
+ }
712
+ else {
713
+ const button = element("button", id);
714
+ button.setAttribute("type", "button");
715
+ button.dataset.citedId = id;
716
+ button.addEventListener("click", () => {
717
+ detail.replaceChildren();
718
+ renderCitedRecord(target, records, detail);
719
+ });
720
+ item.append(button);
721
+ }
722
+ list.append(item);
723
+ }
724
+ host.append(list, detail);
725
+ return list;
726
+ }
727
+ const CLAIM_TYPE_LABEL = (claim) => claim.recognized ? claim.type : `unrecognized (${claim.type})`;
728
+ // A recomputed headline number never wears the producer's value. When the
729
+ // two disagree the recomputed value is drawn and a `count mismatch` marker
730
+ // sits beside it; the stated value is kept on the marker's data attribute,
731
+ // never in the text a reader takes as the number.
732
+ function countMismatchMarker(result, field) {
733
+ const mismatch = result.countMismatches.find((entry) => entry.field === field);
734
+ if (mismatch === undefined)
735
+ return undefined;
736
+ const marker = element("span", "count mismatch");
737
+ marker.dataset.countMismatch = field;
738
+ marker.dataset.stated = String(mismatch.stated);
739
+ marker.dataset.recomputed = String(mismatch.recomputed);
740
+ return marker;
741
+ }
742
+ // The close state a reader sees is the one this bundle's links read
743
+ // whenever the cited Close is supplied; the Result's own value is drawn
744
+ // only when it is not, and then under a `producer-asserted` marker. When
745
+ // the two disagree the recomputed state is drawn with a `state mismatch`
746
+ // marker; the asserted value stays on the data attribute, never in the
747
+ // text. AGREED carries no mark of its own -- its label and the
748
+ // acknowledging peer's record are the whole affordance.
749
+ function appendCloseState(parent, close, tag) {
750
+ const cell = element(tag, close.state);
751
+ cell.dataset.closeState = close.state;
752
+ cell.dataset.closeDerivation = close.derivation;
753
+ cell.dataset.assertedState = close.asserted;
754
+ if (close.stateMismatch) {
755
+ const marker = element("span", "state mismatch");
756
+ marker.dataset.stateMismatch = "close_state";
757
+ marker.dataset.asserted = close.asserted;
758
+ marker.dataset.recomputed = close.state;
759
+ cell.append(" ", marker);
760
+ }
761
+ else if (close.derivation === "producer-asserted") {
762
+ const marker = element("span", "producer-asserted");
763
+ marker.dataset.producerAsserted = "close_state";
764
+ cell.append(" ", marker);
765
+ }
766
+ if (close.peerRefMismatch) {
767
+ const marker = element("span", "peer_close_ref carries no such link");
768
+ marker.dataset.peerRefMismatch = "peer_close_ref";
769
+ cell.append(" ", marker);
770
+ }
771
+ parent.append(cell);
772
+ return cell;
773
+ }
774
+ function renderClose(close, result, host) {
775
+ host.append(element("h4", "Close"));
776
+ const details = element("dl");
777
+ if (close.period !== undefined)
778
+ appendValue(details, "period", `${close.period.start} → ${close.period.end}`);
779
+ details.append(element("dt", "state"));
780
+ appendCloseState(details, close, "dd");
781
+ details.append(element("dt", "derivation"));
782
+ const derivation = element("dd", close.derivation === "recomputed"
783
+ ? `recomputed from ${close.links.length} counterparty ${close.links.length === 1 ? "link" : "links"} to the cited Close in this bundle${close.ignored.length === 0 ? "" : ` (${close.ignored.length} other ${close.ignored.length === 1 ? "link" : "links"} ignored)`}`
784
+ : "producer-asserted: the cited Close is not a record in this bundle, so its links could not be read");
785
+ derivation.dataset.closeDerivationNote = close.derivation;
786
+ details.append(derivation);
787
+ if (close.peer !== undefined)
788
+ appendValue(details, "peer", close.peer);
789
+ if (close.bookId !== undefined)
790
+ appendValue(details, "book", close.bookId);
791
+ // A signer is drawn as verified only when its Producer Envelope verified
792
+ // under the key_id (result-root.ts `signerOf`); otherwise it carries the
793
+ // label, never a bare key.
794
+ if (close.keyId !== undefined) {
795
+ details.append(element("dt", "signer"));
796
+ const signer = element("dd", close.keyVerified === true
797
+ ? `${close.keyId} (verified)`
798
+ : `${close.keyId} · ${UNVERIFIED_KEY_LABEL}`);
799
+ signer.dataset.keyVerified = String(close.keyVerified === true);
800
+ details.append(signer);
801
+ }
802
+ host.append(details);
803
+ host.append(element("h5", "Cited Close"));
804
+ renderCitationList([close.closeRef], result.records, host);
805
+ if (close.peerCloseRef !== undefined) {
806
+ host.append(element("h5", "Peer record"));
807
+ renderCitationList([close.peerCloseRef], result.records, host);
808
+ }
809
+ if (close.links.length > 0) {
810
+ host.append(element("h5", "Links to the cited Close"));
811
+ const list = element("ul");
812
+ for (const link of close.links) {
813
+ const item = element("li", `${link.type} · ${link.recordId} · signer ${link.keyId} (verified)`);
814
+ item.dataset.closeLink = link.type;
815
+ item.dataset.linkRecord = link.recordId;
816
+ list.append(item);
817
+ }
818
+ host.append(list);
819
+ }
820
+ // Inbound links that made no state -- from the Close's own book, a book
821
+ // that is not the named peer, or the Close's own key -- are listed with
822
+ // the reason, never counted: a reader sees why AGREED was not read.
823
+ if (close.ignored.length > 0) {
824
+ host.append(element("h5", "Links ignored (not from the counterparty)"));
825
+ const list = element("ul");
826
+ for (const link of close.ignored) {
827
+ const item = element("li", `${link.type} · ${link.recordId} · ${link.reason}`);
828
+ item.dataset.ignoredLink = link.type;
829
+ item.dataset.linkRecord = link.recordId;
830
+ list.append(item);
831
+ }
832
+ host.append(list);
833
+ }
834
+ }
835
+ // The verdict a reader sees is the Result's own only when every cited id
836
+ // resolves in this bundle and the claim stands. A claim that FAILED
837
+ // verification (its close state is not what the links read) reads
838
+ // `failed` -- never `met`, never its stated verdict, which is kept on
839
+ // `data-stated-verdict` only. Otherwise an unresolved citation reads
840
+ // `unsupported`. The row stays in every case.
841
+ function appendVerdict(parent, claim) {
842
+ const shown = claim.failed
843
+ ? "failed"
844
+ : claim.support === "supported"
845
+ ? claim.verdict
846
+ : "unsupported";
847
+ const cell = element(parent.tagName === "TR" ? "td" : "dd", shown);
848
+ cell.dataset.verdict = shown;
849
+ cell.dataset.support = claim.support;
850
+ if (claim.failed) {
851
+ cell.dataset.statedVerdict = claim.verdict;
852
+ cell.className = "claim-failed";
853
+ }
854
+ else if (claim.support === "unsupported")
855
+ cell.className = "claim-unsupported";
856
+ parent.append(cell);
857
+ return cell;
858
+ }
859
+ // A failed claim's sufficiency is the producer's word too: the cell reads
860
+ // `failed`, the stated value on `data-stated-sufficiency` only.
861
+ function appendSufficiency(parent, claim) {
862
+ const shown = claim.failed ? "failed" : claim.sufficiency;
863
+ const cell = element(parent.tagName === "TR" ? "td" : "dd", shown);
864
+ cell.dataset.sufficiency = shown;
865
+ if (claim.failed) {
866
+ cell.dataset.statedSufficiency = claim.sufficiency;
867
+ cell.className = "claim-failed";
868
+ }
869
+ parent.append(cell);
870
+ return cell;
871
+ }
872
+ function renderClaim(claim, result, host) {
873
+ host.replaceChildren();
874
+ host.append(element("h3", `Claim ${claim.id}`));
875
+ const details = element("dl");
876
+ appendValue(details, "contract", claim.contractRef);
877
+ appendValue(details, "requirement", claim.requirementRef);
878
+ details.append(element("dt", "type"));
879
+ const type = element("dd", CLAIM_TYPE_LABEL(claim));
880
+ type.dataset.claimType = claim.recognized ? claim.type : "unrecognized";
881
+ if (!claim.recognized)
882
+ type.className = "claim-unrecognized";
883
+ details.append(type);
884
+ appendValue(details, "tier", claim.tier);
885
+ appendValue(details, "grade", claim.grade);
886
+ details.append(element("dt", "sufficiency"));
887
+ appendSufficiency(details, claim);
888
+ details.append(element("dt", "verdict"));
889
+ appendVerdict(details, claim);
890
+ host.append(details);
891
+ if (claim.failed) {
892
+ const note = element("p", `failed: ${claim.failure ?? "verification failed"}; sufficiency and verdict withheld`);
893
+ note.dataset.claimFailed = claim.failedOn ?? "close_state";
894
+ host.append(note);
895
+ }
896
+ if (claim.close !== undefined)
897
+ renderClose(claim.close, result, host);
898
+ if (claim.support === "unsupported") {
899
+ const note = element("p", claim.missing.length === 0
900
+ ? "unsupported: this claim cites no evidence"
901
+ : `unsupported: cited evidence not in this bundle: ${claim.missing.join(", ")}`);
902
+ note.dataset.claimMissing = String(claim.missing.length);
903
+ host.append(note);
904
+ }
905
+ host.append(element("h4", "Presentation"));
906
+ const carrier = element("dl");
907
+ appendValue(carrier, "carrier", claim.presentation.kind);
908
+ appendValue(carrier, "status", claim.presentation.status);
909
+ if (claim.presentation.summary !== undefined)
910
+ appendValue(carrier, "summary", claim.presentation.summary);
911
+ if (claim.presentation.narrative !== undefined)
912
+ appendValue(carrier, "narrative", claim.presentation.narrative);
913
+ host.append(carrier);
914
+ // A disclosure carrier names the digests it discloses. They are drawn in
915
+ // the same row shape as any other citation, resolved against this bundle;
916
+ // the claim's own `evidence[]` below is the list the verdict rests on.
917
+ if (claim.presentation.evidence !== undefined) {
918
+ host.append(element("h5", "Carrier evidence"));
919
+ const list = renderCitationList(claim.presentation.evidence, result.records, host);
920
+ list.dataset.carrierEvidence = String(claim.presentation.evidence.length);
921
+ }
922
+ host.append(element("h4", "Proofs"));
923
+ if (claim.proofs.length === 0) {
924
+ host.append(element("p", "no proof cited"));
925
+ }
926
+ else {
927
+ const proofs = element("ul");
928
+ for (const proof of claim.proofs)
929
+ proofs.append(element("li", `${proof.kind} · ${proof.digest} · not resolved here`));
930
+ host.append(proofs);
931
+ }
932
+ host.append(element("h4", "Evidence"));
933
+ for (const ref of claim.evidence) {
934
+ const record = result.records.get(ref.digest);
935
+ if (record === undefined) {
936
+ const missing = element("p", `${ref.digest} · not in this bundle`);
937
+ missing.dataset.evidence = "missing";
938
+ host.append(missing);
939
+ }
940
+ else {
941
+ renderCitedRecord(record, result.records, host);
942
+ }
943
+ }
944
+ }
945
+ const BUCKETS = [
946
+ ["met", "met"],
947
+ ["notMet", "not met"],
948
+ ["notEvaluable", "not evaluable"],
949
+ ];
950
+ // Coverage is the first thing in the Result section after its heading, and
951
+ // no number precedes it within the section; then the three buckets, never a
952
+ // single figure; then one row per claim with its own tier and grade. The
953
+ // bundle-level verification banner, drawn before every section, is the one
954
+ // thing above coverage that can carry digits ("N of M records
955
+ // uncheckpointed"). Every number drawn here is the recomputed one
956
+ // (`result.coverage`, `result.bucketCounts`); a producer figure the claims
957
+ // do not bear out shows as a `count mismatch` marker beside the recomputed
958
+ // value. `excluded as not applicable` is the one figure carried as stated:
959
+ // no claim backs it, by construction.
960
+ function renderResultPage(result, root) {
961
+ const section = element("section");
962
+ section.dataset.page = "result";
963
+ section.append(element("h1", "Evidence result"));
964
+ const coverage = element("p");
965
+ coverage.append(`coverage: ${result.coverage.evaluatedPopulation} requirements evaluated`);
966
+ const evaluatedMarker = countMismatchMarker(result, "evaluated_population");
967
+ if (evaluatedMarker !== undefined)
968
+ coverage.append(" ", evaluatedMarker);
969
+ coverage.append(` · ${result.coverage.excludedNotApplicable} excluded as not applicable · ${result.coverage.unknownCount} unresolved`);
970
+ const unknownMarker = countMismatchMarker(result, "unknown_count");
971
+ if (unknownMarker !== undefined)
972
+ coverage.append(" ", unknownMarker);
973
+ coverage.dataset.coverage = "result";
974
+ coverage.dataset.evaluated = String(result.coverage.evaluatedPopulation);
975
+ coverage.dataset.excluded = String(result.coverage.excludedNotApplicable);
976
+ coverage.dataset.excludedBasis = "stated";
977
+ coverage.dataset.unknown = String(result.coverage.unknownCount);
978
+ section.append(coverage);
979
+ const byId = new Map(result.claims.map((claim) => [claim.id, claim]));
980
+ const buckets = element("section");
981
+ buckets.dataset.buckets = "verdict";
982
+ buckets.append(element("h2", "Claims by verdict"));
983
+ for (const [key, label] of BUCKETS) {
984
+ const count = result.bucketCounts[key];
985
+ const heading = element("h3", `${label}: ${count === 0 ? "none" : count}`);
986
+ heading.dataset.bucketCount = String(count);
987
+ heading.dataset.bucketOf = key;
988
+ const marker = countMismatchMarker(result, `buckets.${key === "notMet" ? "not_met" : key === "notEvaluable" ? "not_evaluable" : "met"}`);
989
+ if (marker !== undefined)
990
+ heading.append(" ", marker);
991
+ buckets.append(heading);
992
+ const ids = result.buckets[key];
993
+ if (ids.length === 0) {
994
+ buckets.append(element("p", "none"));
995
+ continue;
996
+ }
997
+ const list = element("ul");
998
+ list.dataset.bucket = key;
999
+ for (const id of ids) {
1000
+ const claim = byId.get(id);
1001
+ const item = element("li", claim?.failed
1002
+ ? `${id} · failed`
1003
+ : claim?.support === "unsupported"
1004
+ ? `${id} · unsupported`
1005
+ : id);
1006
+ item.dataset.claimRef = id;
1007
+ if (claim?.failed)
1008
+ item.className = "claim-failed";
1009
+ else if (claim?.support === "unsupported")
1010
+ item.className = "claim-unsupported";
1011
+ list.append(item);
1012
+ }
1013
+ buckets.append(list);
1014
+ }
1015
+ // The verifier's own state, after the producer's three buckets: claims
1016
+ // that failed verification (a close state the links contradict). Drawn
1017
+ // always, `none` when empty, so a reader never has to infer from an
1018
+ // absent heading that nothing failed. Not a verdict bucket: a failed
1019
+ // claim is counted here and under no verdict, whatever bucket the
1020
+ // producer listed it in (that listing stays above, marked).
1021
+ const failedHeading = element("h3", `failed: ${result.bucketCounts.failed === 0 ? "none" : result.bucketCounts.failed}`);
1022
+ failedHeading.dataset.bucketCount = String(result.bucketCounts.failed);
1023
+ failedHeading.dataset.bucketOf = "failed";
1024
+ buckets.append(failedHeading);
1025
+ const failedIds = result.claims
1026
+ .filter((claim) => claim.failed)
1027
+ .map((claim) => claim.id);
1028
+ if (failedIds.length === 0)
1029
+ buckets.append(element("p", "none"));
1030
+ else {
1031
+ const list = element("ul");
1032
+ list.dataset.bucket = "failed";
1033
+ for (const id of failedIds) {
1034
+ const item = element("li", id);
1035
+ item.dataset.claimRef = id;
1036
+ item.className = "claim-failed";
1037
+ list.append(item);
1038
+ }
1039
+ buckets.append(list);
1040
+ }
1041
+ section.append(buckets);
1042
+ const table = document.createElement("table");
1043
+ table.dataset.claims = "rows";
1044
+ const detail = element("section");
1045
+ for (const claim of result.claims) {
1046
+ const tr = document.createElement("tr");
1047
+ tr.dataset.claimRow = claim.id;
1048
+ tr.dataset.support = claim.support;
1049
+ if (claim.support === "unsupported")
1050
+ tr.className = "claim-unsupported";
1051
+ if (claim.failed) {
1052
+ tr.classList.add("claim-failed");
1053
+ tr.dataset.failed = claim.failedOn ?? "close_state";
1054
+ }
1055
+ const idCell = document.createElement("td");
1056
+ const button = element("button", claim.id);
1057
+ button.setAttribute("type", "button");
1058
+ button.dataset.claimId = claim.id;
1059
+ button.addEventListener("click", () => renderClaim(claim, result, detail));
1060
+ idCell.append(button);
1061
+ tr.append(idCell, element("td", claim.requirementRef));
1062
+ appendSufficiency(tr, claim);
1063
+ appendVerdict(tr, claim);
1064
+ const tier = element("td", claim.tier);
1065
+ tier.dataset.tier = claim.tier;
1066
+ const grade = element("td", claim.grade);
1067
+ grade.dataset.grade = claim.grade;
1068
+ const type = element("td", CLAIM_TYPE_LABEL(claim));
1069
+ type.dataset.claimType = claim.recognized ? claim.type : "unrecognized";
1070
+ if (!claim.recognized)
1071
+ type.className = "claim-unrecognized";
1072
+ if (claim.close !== undefined) {
1073
+ type.append(" · ");
1074
+ appendCloseState(type, claim.close, "span");
1075
+ }
1076
+ tr.append(tier, grade, type);
1077
+ table.append(tr);
1078
+ }
1079
+ section.append(table, detail, renderProvenance(result.capsuleId, result.logCoordinates, result));
1080
+ root.append(section);
1081
+ }
1082
+ function renderGraph(graph, root, records) {
1083
+ const aggregate = element("section");
1084
+ aggregate.append(element("h1", "Evidence graph"));
1085
+ const metrics = element("dl");
1086
+ appendValue(metrics, "per-axis", graph.aggregate.perAxis);
1087
+ appendValue(metrics, "counts", graph.aggregate.counts);
1088
+ aggregate.append(metrics);
1089
+ root.append(aggregate, renderCalibration(graph.calibration));
1090
+ const calendar = element("section");
1091
+ calendar.append(element("h2", "Daily reports"));
1092
+ const detail = element("section");
1093
+ [...graph.reports]
1094
+ .sort((left, right) => left.date.localeCompare(right.date))
1095
+ .forEach((report) => {
1096
+ // The label is the report's date as the producer wrote it -- a bare
1097
+ // day stays a day, a timestamp stays a timestamp, and one that states
1098
+ // no zone is marked so rather than tiled under an assigned one.
1099
+ const tile = element("button");
1100
+ tile.append(renderTime(report.date), `: ${metRate(report)}`);
1101
+ tile.setAttribute("type", "button");
1102
+ tile.dataset.reportDate = report.date;
1103
+ tile.addEventListener("click", () => renderReport(report, detail, records));
1104
+ calendar.append(tile);
1105
+ });
1106
+ root.append(calendar, detail);
1107
+ }
1108
+ // ---------------------------------------------------------------------------
1109
+ // The built-in presentations (spec/presentation-contract-v0.md appendix A).
1110
+ // Which one renders is decided by their manifests (builtin-manifests.ts),
1111
+ // never here: each module only builds its model and draws it.
1112
+ // ---------------------------------------------------------------------------
1113
+ // A model built by canRender is kept for buildModel, per context, so a
1114
+ // module never builds twice and canRender stays the one place that decides.
1115
+ function memo(build) {
1116
+ const cache = new WeakMap();
1117
+ return (context) => {
1118
+ let value = cache.get(context);
1119
+ if (value === undefined) {
1120
+ value = build(context);
1121
+ cache.set(context, value);
1122
+ }
1123
+ return value;
1124
+ };
1125
+ }
1126
+ const reportRowsOf = memo(buildReportRows);
1127
+ // A Result root whose document is malformed makes buildResultRoot throw. The
1128
+ // throw rejects renderEvidenceGraph before anything is written, exactly as
1129
+ // before the registry: a malformed Result is refused, never rendered as one.
1130
+ const resultRootOf = memo(async (context) => (await isResultRoot(context)) ? buildResultRoot(context) : undefined);
1131
+ // EvidenceGraphError means "no evaluation-summary graph here" (the module
1132
+ // declines and the fallback renders); any other error propagates.
1133
+ const evidenceGraphOf = memo(async (context) => {
1134
+ try {
1135
+ return await buildEvidenceGraph(context);
1136
+ }
1137
+ catch (err) {
1138
+ if (!(err instanceof EvidenceGraphError))
1139
+ throw err;
1140
+ return undefined;
1141
+ }
1142
+ });
1143
+ function defined(value, id) {
1144
+ if (value === undefined)
1145
+ throw new Error(`${id}: buildModel called although canRender was false`);
1146
+ return value;
1147
+ }
1148
+ const reportRowsModule = {
1149
+ manifest: BUILTIN_MANIFEST_REPORT_ROWS,
1150
+ canRender: async (context) => (await reportRowsOf(context)) !== undefined,
1151
+ buildModel: async (context) => defined(await reportRowsOf(context), BUILTIN_MANIFEST_REPORT_ROWS.id),
1152
+ render: (model, host) => renderReportRowsTable(model, host.L1),
1153
+ };
1154
+ const resultOutcomeReportModule = {
1155
+ manifest: BUILTIN_MANIFEST_RESULT_OUTCOME_REPORT,
1156
+ canRender: async (context) => readOutcomeReportPresentation(context.bundle) !== undefined &&
1157
+ (await isResultRoot(context)),
1158
+ buildModel: async (context) => ({
1159
+ result: defined(await resultRootOf(context), BUILTIN_MANIFEST_RESULT_OUTCOME_REPORT.id),
1160
+ card: defined(readOutcomeReportPresentation(context.bundle), BUILTIN_MANIFEST_RESULT_OUTCOME_REPORT.id),
1161
+ bundle: context.bundle,
1162
+ verification: context.verification,
1163
+ }),
1164
+ render: async (model, host) => {
1165
+ // The banner and the verification page take the card's look.
1166
+ host.setChromeClass("oi");
1167
+ await renderOutcomeReportPage(model.result, model.card, model.bundle, model.verification, host.L1);
1168
+ },
1169
+ };
1170
+ const resultComplianceModule = {
1171
+ manifest: BUILTIN_MANIFEST_RESULT_COMPLIANCE,
1172
+ canRender: async (context) => readCompliancePresentation(context.bundle) !== undefined &&
1173
+ (await isResultRoot(context)),
1174
+ buildModel: async (context) => ({
1175
+ result: defined(await resultRootOf(context), BUILTIN_MANIFEST_RESULT_COMPLIANCE.id),
1176
+ card: defined(readCompliancePresentation(context.bundle), BUILTIN_MANIFEST_RESULT_COMPLIANCE.id),
1177
+ bundle: context.bundle,
1178
+ verification: context.verification,
1179
+ }),
1180
+ render: (model, host) => renderCompliancePage(model.result, model.card, model.bundle, model.verification, host.L1),
1181
+ };
1182
+ const resultModule = {
1183
+ manifest: BUILTIN_MANIFEST_RESULT,
1184
+ canRender: (context) => isResultRoot(context),
1185
+ buildModel: async (context) => defined(await resultRootOf(context), BUILTIN_MANIFEST_RESULT.id),
1186
+ render: (model, host) => renderResultPage(model, host.L1),
1187
+ };
1188
+ const evaluationSummaryGraphModule = {
1189
+ manifest: BUILTIN_MANIFEST_EVALUATION_SUMMARY_GRAPH,
1190
+ canRender: async (context) => (await evidenceGraphOf(context)) !== undefined,
1191
+ buildModel: async (context) => {
1192
+ const records = object(context.bundle).records;
1193
+ return {
1194
+ graph: defined(await evidenceGraphOf(context), BUILTIN_MANIFEST_EVALUATION_SUMMARY_GRAPH.id),
1195
+ records: Array.isArray(records) ? records : [],
1196
+ };
1197
+ },
1198
+ render: (model, host) => renderGraph(model.graph, host.L1, model.records),
1199
+ };
1200
+ // The evaluation-summary/v1 aggregate is optional: a verified bundle whose
1201
+ // root is none of the other families (a deal root, for example) renders
1202
+ // without the aggregate panel and says so, instead of rendering nothing.
1203
+ const noAggregateModule = {
1204
+ manifest: BUILTIN_MANIFEST_NO_AGGREGATE,
1205
+ canRender: () => true,
1206
+ buildModel: () => null,
1207
+ render: (_model, host) => {
1208
+ const note = document.createElement("p");
1209
+ note.setAttribute("data-notice", "no-aggregate");
1210
+ note.textContent =
1211
+ "This bundle carries no evaluation summary, so there is no aggregate view. The records and their verification are below.";
1212
+ host.L1.append(note);
1213
+ },
1214
+ };
1215
+ // The extension interpreters each built-in module runs when it renders: a
1216
+ // Result page reads the book payload extensions, and each card reads its own
1217
+ // block. The rows table, the graph and the no-aggregate note read none.
1218
+ const BUILTIN_MODULE_INTERPRETERS = Object.freeze({
1219
+ [BUILTIN_MANIFEST_RESULT_OUTCOME_REPORT.id]: [
1220
+ "result-root",
1221
+ "outcome-report-card",
1222
+ ],
1223
+ [BUILTIN_MANIFEST_RESULT_COMPLIANCE.id]: ["result-root", "compliance-card"],
1224
+ [BUILTIN_MANIFEST_RESULT.id]: ["result-root"],
1225
+ [BUILTIN_MANIFEST_COMPOSED.id]: ["aac.builtin.composed/v0"],
1226
+ });
1227
+ /** The six built-in modules, registered by default. */
1228
+ export const BUILTIN_PRESENTATIONS = Object.freeze([
1229
+ reportRowsModule,
1230
+ resultOutcomeReportModule,
1231
+ resultComplianceModule,
1232
+ resultModule,
1233
+ evaluationSummaryGraphModule,
1234
+ noAggregateModule,
1235
+ ]);
1236
+ /** A fresh registry holding the six built-in modules. */
1237
+ export function createPresentationRegistry() {
1238
+ const registry = new PresentationRegistry();
1239
+ for (const module of BUILTIN_PRESENTATIONS)
1240
+ registry.register(module);
1241
+ return registry;
1242
+ }
1243
+ const defaultRegistry = createPresentationRegistry();
1244
+ /**
1245
+ * The built-in sections: modules resolved after the page module, in their
1246
+ * own registry, and rendered after its content. One today, the composition
1247
+ * section; see BUILTIN_MANIFEST_COMPOSED for why it is not a page module.
1248
+ */
1249
+ export const BUILTIN_SECTIONS = Object.freeze([
1250
+ composedSectionModule,
1251
+ ]);
1252
+ /**
1253
+ * A fresh section registry holding the built-in sections. The section
1254
+ * registry applies the presentation ABI as the page registry does: under a
1255
+ * `runtime` that refuses a section, the section is registered as refused,
1256
+ * never drawn, and every page it matches names it.
1257
+ */
1258
+ export function createSectionRegistry(runtime) {
1259
+ const registry = new PresentationRegistry(runtime);
1260
+ for (const module of BUILTIN_SECTIONS)
1261
+ registry.register(module);
1262
+ return registry;
1263
+ }
1264
+ const defaultSections = createSectionRegistry();
1265
+ /**
1266
+ * Register a module with the registry `renderEvidenceGraph` uses by default
1267
+ * (the module-slot entry point). Throws when its manifest is malformed,
1268
+ * dead, a duplicate, or co-matchable with a registered module of its tier.
1269
+ * A module whose `presentation_api` this runtime does not implement (or
1270
+ * whose `runtime_min` it does not meet) is registered as refused: it is
1271
+ * never selected, and every page it would have rendered names it.
1272
+ */
1273
+ export function registerPresentation(module) {
1274
+ return defaultRegistry.register(module);
1275
+ }
1276
+ /** Resolve over the default registry (spec section 4.3). */
1277
+ export function resolvePresentation(context, audience = DEFAULT_AUDIENCE, format = DEFAULT_FORMAT) {
1278
+ return defaultRegistry.resolve(context, audience, format);
1279
+ }
1280
+ /** `"*"`: no particular audience, so only modules serving every audience. */
1281
+ const DEFAULT_AUDIENCE = "*";
1282
+ const DEFAULT_FORMAT = "html";
1283
+ /**
1284
+ * True when the rendered page module checked the producer signature of the
1285
+ * records it cites as signers: the built-in Result page, on a Close (or a
1286
+ * record linking to it) that carries a `key_id` (result-root.ts `signerOf`).
1287
+ */
1288
+ function citesCheckedSigners(selected) {
1289
+ if (selected === undefined || selected.module !== resultModule)
1290
+ return false;
1291
+ return selected.model.claims.some((claim) => claim.close !== undefined &&
1292
+ (claim.close.keyId !== undefined ||
1293
+ [...claim.close.links, ...claim.close.ignored].some((link) => link.keyId !== undefined)));
1294
+ }
1295
+ const OUTCOME_CHROME = "oi";
1296
+ function presentationServices(context) {
1297
+ return Object.freeze({
1298
+ details(summary, open = false) {
1299
+ const details = document.createElement("details");
1300
+ if (open)
1301
+ details.open = true;
1302
+ details.append(element("summary", summary));
1303
+ return details;
1304
+ },
1305
+ badge(state) {
1306
+ const badge = element("span", state);
1307
+ badge.dataset.badge = state;
1308
+ return badge;
1309
+ },
1310
+ disclosure(capsuleId, member) {
1311
+ return disclosureOf(context, capsuleId, member).state;
1312
+ },
1313
+ });
1314
+ }
1315
+ // The refusal for a verified bundle whose presentation could not be resolved
1316
+ // (an ambiguity) or failed (a module threw while rendering): the core's
1317
+ // words, no module content, the verification page below (contract I1).
1318
+ const PRESENTATION_REFUSALS = {
1319
+ "presentation-unresolved": "The presentation for this bundle could not be resolved. No presentation content is shown; the verification page below lists the checks.",
1320
+ "presentation-failed": "The presentation for this bundle failed. No presentation content is shown; the verification page below lists the checks.",
1321
+ };
1322
+ export async function renderEvidenceGraph(input, root, countersigners, options = {}) {
1323
+ // With no SHA-256 nothing can be verified: say so, never "failed".
1324
+ if (!isVerifiedBundleContext(input) && digestCapability() === "none") {
1325
+ renderUnableToVerify(root, options.verifyCommand === undefined
1326
+ ? {}
1327
+ : { verifyCommand: options.verifyCommand });
1328
+ return;
1329
+ }
1330
+ // Verify first. Nothing reaches the DOM until the verification result is
1331
+ // in hand, and no module is consulted for a bundle that did not verify.
1332
+ const context = isVerifiedBundleContext(input)
1333
+ ? countersigners === undefined
1334
+ ? input
1335
+ : withCountersigners(input, countersigners)
1336
+ : await buildVerifiedBundleContext(input, {
1337
+ ...(countersigners === undefined ? {} : { countersigners }),
1338
+ });
1339
+ const { bundle, verification } = context;
1340
+ const verified = bundleVerified(verification);
1341
+ const registry = options.registry ?? defaultRegistry;
1342
+ let refusal;
1343
+ let selected;
1344
+ let resolution;
1345
+ try {
1346
+ resolution = await registry.resolve(context, options.audience ?? DEFAULT_AUDIENCE, options.format ?? DEFAULT_FORMAT);
1347
+ }
1348
+ catch (err) {
1349
+ if (!(err instanceof PresentationAmbiguityError))
1350
+ throw err;
1351
+ resolution = { kind: "no-presentation", refused: [] };
1352
+ refusal = "presentation-unresolved";
1353
+ }
1354
+ // A module this runtime refused (spec section 3.2) is never silent: the
1355
+ // page root names every one that matched, page and section alike, and the
1356
+ // verification page shows the contract's words in the extension rows
1357
+ // (presentationRefusalRow) or, for a module requiring no extension, in a
1358
+ // line after them (presentationRefusalLine).
1359
+ const refused = resolution.kind === "refusal" ? [] : [...resolution.refused];
1360
+ if (resolution.kind === "refusal" && verified)
1361
+ throw new Error("resolver refused a bundle that verified");
1362
+ if (resolution.kind === "module" && !verified)
1363
+ throw new Error("resolver selected a module for an unverified bundle");
1364
+ // A module's model is built before the page is written, so a model that
1365
+ // cannot be built rejects with nothing drawn (as it always has).
1366
+ if (resolution.kind === "module")
1367
+ selected = {
1368
+ module: resolution.module,
1369
+ model: await resolution.module.buildModel(context),
1370
+ };
1371
+ // The section registry is resolved the same way, after the page and only
1372
+ // for a verified bundle whose page resolved: a section never stands in for
1373
+ // a refusal (contract I1), and its ambiguity is the page's refusal too.
1374
+ let section;
1375
+ if (verified && refusal === undefined) {
1376
+ let sectionResolution;
1377
+ try {
1378
+ sectionResolution = await (options.sections ?? defaultSections).resolve(context, options.audience ?? DEFAULT_AUDIENCE, options.format ?? DEFAULT_FORMAT);
1379
+ }
1380
+ catch (err) {
1381
+ if (!(err instanceof PresentationAmbiguityError))
1382
+ throw err;
1383
+ sectionResolution = { kind: "no-presentation", refused: [] };
1384
+ selected = undefined;
1385
+ refusal = "presentation-unresolved";
1386
+ }
1387
+ // A refused section is named like a refused page module, never skipped.
1388
+ if (sectionResolution.kind !== "refusal")
1389
+ refused.push(...sectionResolution.refused);
1390
+ if (sectionResolution.kind === "module")
1391
+ section = {
1392
+ module: sectionResolution.module,
1393
+ model: await sectionResolution.module.buildModel(context),
1394
+ };
1395
+ }
1396
+ const records = object(bundle).records;
1397
+ const coverage = {
1398
+ uncheckpointed: unboundRecordIds(verification).length,
1399
+ total: Array.isArray(records) ? records.length : 0,
1400
+ };
1401
+ const drawFrame = () => {
1402
+ root.replaceChildren();
1403
+ renderPresentationHeader(root, bundle);
1404
+ return renderVerificationBanner(root, verified, coverage);
1405
+ };
1406
+ let banner = drawFrame();
1407
+ if (refused.length > 0)
1408
+ root.dataset.presentationRefused = refused.map((r) => r.id).join(" ");
1409
+ else
1410
+ delete root.dataset.presentationRefused;
1411
+ let chrome;
1412
+ if (selected !== undefined) {
1413
+ const host = {
1414
+ L0: root,
1415
+ L1: root,
1416
+ L2: root,
1417
+ depth: options.depth ?? "L2",
1418
+ setChromeClass(name) {
1419
+ if (name !== OUTCOME_CHROME)
1420
+ throw new Error(`unknown chrome class ${JSON.stringify(name)}`);
1421
+ chrome = name;
1422
+ styleBanner(banner, verified);
1423
+ },
1424
+ };
1425
+ try {
1426
+ await selected.module.render(selected.model, host, presentationServices(context));
1427
+ }
1428
+ catch {
1429
+ // Everything the module wrote is discarded; no other module is tried.
1430
+ chrome = undefined;
1431
+ banner = drawFrame();
1432
+ refusal = "presentation-failed";
1433
+ }
1434
+ }
1435
+ if (refusal === undefined && resolution.kind === "no-presentation") {
1436
+ const note = element("p", "No presentation is available for this bundle, audience and format. The records and their verification are below.");
1437
+ note.dataset.notice = "no-presentation";
1438
+ root.append(note);
1439
+ }
1440
+ // The section renders after the page module, into the same regions. If it
1441
+ // throws, everything any module wrote is discarded for the refusal.
1442
+ if (section !== undefined && refusal === undefined) {
1443
+ try {
1444
+ await section.module.render(section.model, {
1445
+ L0: root,
1446
+ L1: root,
1447
+ L2: root,
1448
+ depth: "L2",
1449
+ setChromeClass() {
1450
+ throw new Error("a section has no chrome");
1451
+ },
1452
+ }, presentationServices(context));
1453
+ }
1454
+ catch {
1455
+ chrome = undefined;
1456
+ banner = drawFrame();
1457
+ refusal = "presentation-failed";
1458
+ }
1459
+ }
1460
+ if (refusal !== undefined) {
1461
+ const note = element("p", PRESENTATION_REFUSALS[refusal]);
1462
+ note.dataset.refusal = refusal;
1463
+ root.append(note);
1464
+ }
1465
+ // The interpreters this rendering actually ran (see extension-interpreters.ts):
1466
+ // the presentation header and the countersignature stamp always run; a
1467
+ // module's own interpreters only when that module rendered.
1468
+ const applied = new Set([
1469
+ "presentation-header",
1470
+ "countersignature-stamp",
1471
+ ]);
1472
+ if (refusal === undefined)
1473
+ for (const ran of [selected, section])
1474
+ if (ran !== undefined)
1475
+ for (const id of BUILTIN_MODULE_INTERPRETERS[ran.module.manifest.id] ??
1476
+ [])
1477
+ applied.add(id);
1478
+ await renderVerificationPage(root, context, chrome === OUTCOME_CHROME, applied, refused, refusal === undefined && citesCheckedSigners(selected), options.verifierHint);
1479
+ }
1480
+ //# sourceMappingURL=evidence-graph-view.js.map