@action-state-group/capsule-viewer 0.0.0 → 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.
- package/LICENSE +201 -0
- package/LICENSE-BSD-3-Clause +28 -0
- package/README.md +23 -1
- package/dist/browser.d.ts +4 -0
- package/dist/browser.js +11 -0
- package/dist/browser.js.map +1 -0
- package/dist/compliance-presentation.d.ts +51 -0
- package/dist/compliance-presentation.js +140 -0
- package/dist/compliance-presentation.js.map +1 -0
- package/dist/compliance-styles.d.ts +8 -0
- package/dist/compliance-styles.js +66 -0
- package/dist/compliance-styles.js.map +1 -0
- package/dist/compliance-view.d.ts +6 -0
- package/dist/compliance-view.js +394 -0
- package/dist/compliance-view.js.map +1 -0
- package/dist/compliance.d.ts +92 -0
- package/dist/compliance.js +333 -0
- package/dist/compliance.js.map +1 -0
- package/dist/composed-view.d.ts +24 -0
- package/dist/composed-view.js +179 -0
- package/dist/composed-view.js.map +1 -0
- package/dist/emitter-shell.html +136 -0
- package/dist/emitter.d.ts +69 -0
- package/dist/emitter.js +266 -0
- package/dist/emitter.js.map +1 -0
- package/dist/evidence-graph-view.d.ts +96 -0
- package/dist/evidence-graph-view.js +1480 -0
- package/dist/evidence-graph-view.js.map +1 -0
- package/dist/evidence-graph.iife.js +11373 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/outcome-report-styles.d.ts +16 -0
- package/dist/outcome-report-styles.js +225 -0
- package/dist/outcome-report-styles.js.map +1 -0
- package/dist/outcome-report-view.d.ts +5 -0
- package/dist/outcome-report-view.js +885 -0
- package/dist/outcome-report-view.js.map +1 -0
- package/dist/presentation-builder.d.ts +247 -0
- package/dist/presentation-builder.js +552 -0
- package/dist/presentation-builder.js.map +1 -0
- package/dist/presentation-mount.d.ts +26 -0
- package/dist/presentation-mount.js +45 -0
- package/dist/presentation-mount.js.map +1 -0
- package/package.json +47 -4
|
@@ -0,0 +1,885 @@
|
|
|
1
|
+
import { buildOutcomeReportModel, NOT_STATED, verifyJudgePin, } from "@action-state-group/agent-action-capsule";
|
|
2
|
+
import { OUTCOME_REPORT_CSS } from "./outcome-report-styles.js";
|
|
3
|
+
import { buildVerificationPageModel } from "@action-state-group/agent-action-capsule";
|
|
4
|
+
/**
|
|
5
|
+
* The outcome-report card, rendered from a verified `ResultRoot`. Called
|
|
6
|
+
* from `evidence-graph-view.ts` in place of `renderResultPage` when the
|
|
7
|
+
* bundle's `outcome-report/v1` extension opts in (see
|
|
8
|
+
* `readOutcomeReportPresentation`) -- after the same verify-first gate every
|
|
9
|
+
* other root family goes through: nothing here ever runs on a bundle that
|
|
10
|
+
* did not verify.
|
|
11
|
+
*
|
|
12
|
+
* The whole card renders inside one `.oi`-classed wrapper, with its own
|
|
13
|
+
* `<style>` (`OUTCOME_REPORT_CSS`) as its first child -- every selector in
|
|
14
|
+
* that stylesheet is scoped under `.oi` so it can never affect, or be
|
|
15
|
+
* affected by, the rest of the emitted page (the verification page that
|
|
16
|
+
* always follows it, or the generic Result page this bundle opted out of).
|
|
17
|
+
* No external reference of any kind: no `<link>`, no web font, no image.
|
|
18
|
+
*/
|
|
19
|
+
function element(tag, text) {
|
|
20
|
+
const value = document.createElement(tag);
|
|
21
|
+
if (text !== undefined)
|
|
22
|
+
value.textContent = text;
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
function el(tag, className, text) {
|
|
26
|
+
const value = element(tag, text);
|
|
27
|
+
value.className = className;
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Icons: inline SVG line drawings, never emoji. An emoji glyph renders as an
|
|
32
|
+
// empty box ("tofu") wherever no colour-emoji font is installed -- headless
|
|
33
|
+
// Chromium on a server, most CI images -- and a report is read exactly there.
|
|
34
|
+
// Built with createElementNS from fixed path data (no markup string, no
|
|
35
|
+
// external reference); stroke follows the surrounding text colour.
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// The SVG namespace is a URI that happens to begin with a scheme; it is never
|
|
38
|
+
// fetched. Assembled at run time so the emitted runtime contains no literal
|
|
39
|
+
// "http://" -- capsule-cli's report tests assert an offline report.html has
|
|
40
|
+
// none, as a cheap guard that the page loads nothing from the network.
|
|
41
|
+
const SVG_NS = ["http", "://www.w3.org/2000/svg"].join("");
|
|
42
|
+
const ICON_PATHS = {
|
|
43
|
+
// an open rulebook
|
|
44
|
+
book: [
|
|
45
|
+
"M4 5.5C4 4.7 4.7 4 5.5 4H11v15H5.5C4.7 19 4 18.3 4 17.5Z",
|
|
46
|
+
"M20 5.5C20 4.7 19.3 4 18.5 4H13v15h5.5c.8 0 1.5-.7 1.5-1.5Z",
|
|
47
|
+
"M6.5 8h2.5M6.5 11h2.5M15 8h2.5M15 11h2.5",
|
|
48
|
+
],
|
|
49
|
+
// a ticked circle
|
|
50
|
+
check: ["M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18Z", "M7.5 12.5l3 3l6-6.5"],
|
|
51
|
+
// a receipt with a torn foot
|
|
52
|
+
receipt: [
|
|
53
|
+
"M6 3h12v18l-2-1.5l-2 1.5l-2-1.5l-2 1.5l-2-1.5L6 21Z",
|
|
54
|
+
"M9 8h6M9 11.5h6M9 15h4",
|
|
55
|
+
],
|
|
56
|
+
// a target
|
|
57
|
+
target: [
|
|
58
|
+
"M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18Z",
|
|
59
|
+
"M12 7a5 5 0 1 0 0 10a5 5 0 1 0 0-10Z",
|
|
60
|
+
"M12 11a1 1 0 1 0 0 2a1 1 0 1 0 0-2Z",
|
|
61
|
+
],
|
|
62
|
+
};
|
|
63
|
+
/** The named icon as an inline SVG, or `undefined` for a name with no drawing (the slot then stays empty, never a raw name or glyph). */
|
|
64
|
+
function icon(name) {
|
|
65
|
+
const paths = ICON_PATHS[name];
|
|
66
|
+
if (paths === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
69
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
70
|
+
svg.setAttribute("width", "20");
|
|
71
|
+
svg.setAttribute("height", "20");
|
|
72
|
+
svg.setAttribute("fill", "none");
|
|
73
|
+
svg.setAttribute("stroke", "currentColor");
|
|
74
|
+
svg.setAttribute("stroke-width", "1.6");
|
|
75
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
76
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
77
|
+
svg.setAttribute("aria-hidden", "true");
|
|
78
|
+
svg.dataset.icon = name;
|
|
79
|
+
for (const d of paths) {
|
|
80
|
+
const path = document.createElementNS(SVG_NS, "path");
|
|
81
|
+
path.setAttribute("d", d);
|
|
82
|
+
svg.append(path);
|
|
83
|
+
}
|
|
84
|
+
return svg;
|
|
85
|
+
}
|
|
86
|
+
const fmtCount = (n) => n.toLocaleString();
|
|
87
|
+
function fmtShare(count, total, percentages) {
|
|
88
|
+
if (percentages)
|
|
89
|
+
return total === 0 ? "–" : `${Math.round((count / total) * 100)}%`;
|
|
90
|
+
return `${fmtCount(count)} of ${fmtCount(total)}`;
|
|
91
|
+
}
|
|
92
|
+
const TIER_LABEL = {
|
|
93
|
+
judged: "judged",
|
|
94
|
+
recomputed: "recomputed",
|
|
95
|
+
unknown: "tier unknown — no claims observed",
|
|
96
|
+
mixed: "tier differs across conversations",
|
|
97
|
+
};
|
|
98
|
+
const VERDICT_TEXT = {
|
|
99
|
+
met: "met",
|
|
100
|
+
not_met: "not met",
|
|
101
|
+
not_evaluable: "not evaluable",
|
|
102
|
+
failed: "failed verification",
|
|
103
|
+
unsupported: "unsupported",
|
|
104
|
+
not_applicable: "not applicable",
|
|
105
|
+
};
|
|
106
|
+
/** `tau2:airline:task-20:trial-0` reads "task 20"; any other id is shown whole. */
|
|
107
|
+
function caseLabel(conversationId) {
|
|
108
|
+
const match = /task-([0-9]+)/u.exec(conversationId);
|
|
109
|
+
return match === null ? conversationId : `task ${match[1]}`;
|
|
110
|
+
}
|
|
111
|
+
function criterionLabel(model, checkId, criterionId) {
|
|
112
|
+
const check = model.terms.checks.find((c) => c.id === checkId);
|
|
113
|
+
const criterion = check?.criteria.find((c) => c.id === criterionId);
|
|
114
|
+
const wording = model.criterionWording.get(`${checkId}.${criterionId}`);
|
|
115
|
+
return {
|
|
116
|
+
label: criterion?.label ?? `${checkId}.${criterionId}`,
|
|
117
|
+
text: wording?.text ?? criterion?.text ?? "",
|
|
118
|
+
question: check?.question ?? checkId,
|
|
119
|
+
source: wording?.source ?? "unstated",
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** The label a criterion's wording carries when it is NOT the sealed text. */
|
|
123
|
+
const WORDING_FALLBACK_LABEL = {
|
|
124
|
+
sealed: "",
|
|
125
|
+
unstated: "Wording not stated: no report in this bundle states it.",
|
|
126
|
+
mixed: "Wording not shown: the reports in this bundle state different wordings.",
|
|
127
|
+
};
|
|
128
|
+
/** One sentence on where the criterion wording on this page comes from. */
|
|
129
|
+
function wordingNote(model) {
|
|
130
|
+
const sources = new Set([...model.criterionWording.values()].map((w) => w.source));
|
|
131
|
+
if (sources.size === 1 && sources.has("sealed"))
|
|
132
|
+
return "Rule wording as sealed in the judge's reports (the text the judge was given).";
|
|
133
|
+
if (!sources.has("sealed"))
|
|
134
|
+
return "Rule wording not stated: the reports in this bundle do not state it.";
|
|
135
|
+
return "Rule wording as sealed in the judge's reports; where the reports do not state it (or disagree), marked as such.";
|
|
136
|
+
}
|
|
137
|
+
/** The producer's "edited" badge for a criterion, when its presentation
|
|
138
|
+
* extension marks one -- presentation text, titled as such, never evidence. */
|
|
139
|
+
function editedBadge(model, ref) {
|
|
140
|
+
const edited = model.options.editedCriteria?.find((e) => e.criterion === ref);
|
|
141
|
+
if (edited === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const badge = el("span", "edited", edited.badge);
|
|
144
|
+
badge.dataset.edited = ref;
|
|
145
|
+
badge.title = "Producer's note (presentation extension), not evidence";
|
|
146
|
+
return badge;
|
|
147
|
+
}
|
|
148
|
+
function renderProducerNote(host, model) {
|
|
149
|
+
const note = model.options.producerNote;
|
|
150
|
+
if (note === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const box = el("aside", "pnote");
|
|
153
|
+
box.dataset.producerNote = "true";
|
|
154
|
+
box.append(el("div", "pn-k", "Producer's note · presentation text, not evidence"));
|
|
155
|
+
box.append(el("h2", "pn-t", note.title));
|
|
156
|
+
const list = el("ul", "pn-l");
|
|
157
|
+
for (const line of note.lines)
|
|
158
|
+
list.append(element("li", line));
|
|
159
|
+
box.append(list);
|
|
160
|
+
box.append(el("p", "pn-src", "Carried on this bundle's outcome-report/v1 presentation extension, outside the signed records: no claim backs this note and no number on this page is computed from it. Everything below is drawn only from the verified bundle."));
|
|
161
|
+
host.append(box);
|
|
162
|
+
}
|
|
163
|
+
function isMiss(verdict) {
|
|
164
|
+
return (verdict === "not_met" || verdict === "failed" || verdict === "unsupported");
|
|
165
|
+
}
|
|
166
|
+
function pill(verdict) {
|
|
167
|
+
const text = verdict === "resolved"
|
|
168
|
+
? "resolved"
|
|
169
|
+
: verdict === "missed"
|
|
170
|
+
? "missed"
|
|
171
|
+
: VERDICT_TEXT[verdict];
|
|
172
|
+
const tone = verdict === "met" || verdict === "resolved"
|
|
173
|
+
? "ok"
|
|
174
|
+
: verdict === "not_applicable"
|
|
175
|
+
? "na"
|
|
176
|
+
: verdict === "not_evaluable"
|
|
177
|
+
? "ne"
|
|
178
|
+
: "bad";
|
|
179
|
+
const value = el("span", `pill ${tone}`, text);
|
|
180
|
+
value.dataset.pill = verdict;
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
const fmtP = (p) => p.toFixed(2);
|
|
184
|
+
/** The probability the judge gave the verdict it returned, from the sealed rationale text; undefined when the text carries none. */
|
|
185
|
+
function verdictProbability(criterion) {
|
|
186
|
+
const probs = criterion.probabilities;
|
|
187
|
+
if (probs === undefined)
|
|
188
|
+
return undefined;
|
|
189
|
+
const key = criterion.claim?.verdict ?? criterion.verdict;
|
|
190
|
+
return probs[key];
|
|
191
|
+
}
|
|
192
|
+
function renderConversation(conversation, model, runInfo) {
|
|
193
|
+
const resolved = conversation.verdict === "met";
|
|
194
|
+
const details = el("details", "conv");
|
|
195
|
+
details.dataset.conversation = conversation.conversationId;
|
|
196
|
+
details.dataset.conversationVerdict = conversation.verdict;
|
|
197
|
+
const misses = conversation.criteria.filter((c) => isMiss(c.verdict));
|
|
198
|
+
const unjudged = conversation.criteria.filter((c) => c.verdict === "not_evaluable");
|
|
199
|
+
const na = conversation.criteria.filter((c) => c.verdict === "not_applicable");
|
|
200
|
+
const summary = el("summary", "conv-s");
|
|
201
|
+
summary.append(el("span", "cid", caseLabel(conversation.conversationId)));
|
|
202
|
+
summary.append(pill(resolved ? "resolved" : "missed"));
|
|
203
|
+
summary.append(el("span", "cwhy", resolved
|
|
204
|
+
? `all nine criteria met or not applicable${na.length > 0 ? ` (${na.length} not applicable)` : ""}`
|
|
205
|
+
: `missed on: ${[...misses, ...(misses.length === 0 ? unjudged : [])]
|
|
206
|
+
.map((c) => criterionLabel(model, c.checkId, c.criterionId).label)
|
|
207
|
+
.join(", ")}`));
|
|
208
|
+
summary.append(el("span", "go", "open ›"));
|
|
209
|
+
details.append(summary);
|
|
210
|
+
// The body (nine rows, nine sealed rationales, the ids) is built the first
|
|
211
|
+
// time this conversation is opened, not up front: the same conversation
|
|
212
|
+
// appears under its report line and under its missed reason, and building
|
|
213
|
+
// every body eagerly multiplies the page's DOM for content nobody opened.
|
|
214
|
+
// Built from the same model either way.
|
|
215
|
+
let built = false;
|
|
216
|
+
details.addEventListener("toggle", () => {
|
|
217
|
+
if (built || !details.open)
|
|
218
|
+
return;
|
|
219
|
+
built = true;
|
|
220
|
+
details.append(conversationBody(conversation, model, runInfo, {
|
|
221
|
+
resolved,
|
|
222
|
+
misses,
|
|
223
|
+
unjudged,
|
|
224
|
+
na,
|
|
225
|
+
}));
|
|
226
|
+
});
|
|
227
|
+
return details;
|
|
228
|
+
}
|
|
229
|
+
function renderTranscript(conversation) {
|
|
230
|
+
const t = conversation.transcript;
|
|
231
|
+
const wrap = el("div", "tx");
|
|
232
|
+
wrap.dataset.transcript = t.state;
|
|
233
|
+
if (t.state !== "verified") {
|
|
234
|
+
wrap.append(el("p", "absent", t.state === "absent"
|
|
235
|
+
? `Not in this bundle. The conversation and its tool calls are sealed in the case record ${conversation.sourceCapsuleId ?? "(not stated by the reports)"}, which this Result does not cite, so this page cannot show them and does not vouch for them.`
|
|
236
|
+
: `Not shown: ${t.reason ?? "the cited transcript does not check"}.`));
|
|
237
|
+
return wrap;
|
|
238
|
+
}
|
|
239
|
+
wrap.append(el("p", "small", `Sealed in record ${t.transcriptRecordId}. Its content digest equals what the judged case capsule ${conversation.sourceCapsuleId} committed to (${t.inputDigest}), checked on this page: this is the conversation the judge read. ${t.turns.length} messages, in order.`));
|
|
240
|
+
const list = el("ol", "turns");
|
|
241
|
+
t.turns.forEach((turn, index) => {
|
|
242
|
+
const item = el("li", `turn ${turn.role === "user" || turn.role === "assistant" || turn.role === "tool" ? turn.role : "other"}`);
|
|
243
|
+
item.dataset.turn = String(index);
|
|
244
|
+
item.dataset.role = turn.role;
|
|
245
|
+
const head = el("div", "who", `msg[${index}] ${turn.role}`);
|
|
246
|
+
item.append(head);
|
|
247
|
+
if (turn.content !== undefined && turn.content.length > 0)
|
|
248
|
+
item.append(el(turn.role === "tool" ? "pre" : "div", "say", turn.content));
|
|
249
|
+
for (const call of turn.toolCalls) {
|
|
250
|
+
const callEl = el("pre", "call", `${call.name}(${call.arguments})`);
|
|
251
|
+
callEl.dataset.toolCall = call.name;
|
|
252
|
+
item.append(callEl);
|
|
253
|
+
}
|
|
254
|
+
list.append(item);
|
|
255
|
+
});
|
|
256
|
+
wrap.append(list);
|
|
257
|
+
return wrap;
|
|
258
|
+
}
|
|
259
|
+
function conversationBody(conversation, model, runInfo, parts) {
|
|
260
|
+
const { resolved, misses, unjudged, na } = parts;
|
|
261
|
+
const body = el("div", "conv-b");
|
|
262
|
+
// 1. What the agent did
|
|
263
|
+
body.append(el("h4", "", "What the agent did"));
|
|
264
|
+
body.append(renderTranscript(conversation));
|
|
265
|
+
// 2. The criteria
|
|
266
|
+
body.append(el("h4", "", "The criteria"));
|
|
267
|
+
const contractRefs = new Set(conversation.criteria.flatMap((c) => c.claim === undefined ? [] : [c.claim.contractRef]));
|
|
268
|
+
body.append(el("p", "small", `Judged under ${[...contractRefs].join(", ") || "no stated contract"}. ${wordingNote(model)} P is the probability the judge gave the verdict it returned.`));
|
|
269
|
+
const table = el("table", "crit");
|
|
270
|
+
table.dataset.criteriaTable = conversation.conversationId;
|
|
271
|
+
const head = element("thead");
|
|
272
|
+
const headRow = element("tr");
|
|
273
|
+
headRow.append(element("th", "Criterion"), element("th", "Verdict"), el("th", "n", "P"), element("th", "Rationale, as sealed"));
|
|
274
|
+
head.append(headRow);
|
|
275
|
+
table.append(head);
|
|
276
|
+
const tbody = element("tbody");
|
|
277
|
+
for (const criterion of conversation.criteria) {
|
|
278
|
+
const tr = element("tr");
|
|
279
|
+
tr.dataset.crit = `${criterion.checkId}.${criterion.criterionId}`;
|
|
280
|
+
tr.dataset.critVerdict = criterion.verdict;
|
|
281
|
+
const names = criterionLabel(model, criterion.checkId, criterion.criterionId);
|
|
282
|
+
const nameCell = el("td", "cn");
|
|
283
|
+
nameCell.append(el("b", "", names.label));
|
|
284
|
+
const rowBadge = editedBadge(model, `${criterion.checkId}.${criterion.criterionId}`);
|
|
285
|
+
if (rowBadge !== undefined)
|
|
286
|
+
nameCell.append(rowBadge);
|
|
287
|
+
const wordingCell = el("small", "", names.text);
|
|
288
|
+
wordingCell.dataset.wording = names.source;
|
|
289
|
+
nameCell.append(wordingCell);
|
|
290
|
+
if (names.source !== "sealed")
|
|
291
|
+
nameCell.append(el("small", "wsrc", WORDING_FALLBACK_LABEL[names.source]));
|
|
292
|
+
tr.append(nameCell);
|
|
293
|
+
const verdictCell = element("td");
|
|
294
|
+
verdictCell.append(pill(criterion.verdict));
|
|
295
|
+
tr.append(verdictCell);
|
|
296
|
+
const p = verdictProbability(criterion);
|
|
297
|
+
const pCell = el("td", "n", p === undefined ? "–" : fmtP(p));
|
|
298
|
+
if (p !== undefined)
|
|
299
|
+
pCell.dataset.p = fmtP(p);
|
|
300
|
+
tr.append(pCell);
|
|
301
|
+
const rationaleCell = el("td", "rat");
|
|
302
|
+
if (criterion.verdict === "not_applicable") {
|
|
303
|
+
rationaleCell.append(el("span", "muted", "No claim: Result v0 excludes a not-applicable judgment from its claims, so this bundle cites no report for it."));
|
|
304
|
+
}
|
|
305
|
+
else if (criterion.rationale !== undefined) {
|
|
306
|
+
const sealed = el("code", "sealed", criterion.rationale);
|
|
307
|
+
sealed.dataset.rationale = "sealed";
|
|
308
|
+
rationaleCell.append(sealed);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
rationaleCell.append(el("span", "muted", "no rationale in the cited report"));
|
|
312
|
+
}
|
|
313
|
+
tr.append(rationaleCell);
|
|
314
|
+
tbody.append(tr);
|
|
315
|
+
}
|
|
316
|
+
table.append(tbody);
|
|
317
|
+
const scroll = el("div", "tscroll");
|
|
318
|
+
scroll.append(table);
|
|
319
|
+
body.append(scroll);
|
|
320
|
+
const honest = el("p", "small", "Rationale: derived from the judge's probabilities; the judge returns no free-text reasoning.");
|
|
321
|
+
honest.dataset.rationaleLabel = "derived";
|
|
322
|
+
body.append(honest);
|
|
323
|
+
// 3. Why
|
|
324
|
+
body.append(el("h4", "", "Why"));
|
|
325
|
+
const why = el("ul", "why");
|
|
326
|
+
why.dataset.why = resolved ? "resolved" : "missed";
|
|
327
|
+
if (resolved) {
|
|
328
|
+
const met = conversation.criteria.filter((c) => c.verdict === "met").length;
|
|
329
|
+
why.append(element("li", `Resolved: ${met} criteria met and ${na.length} not applicable. The rule bills a conversation only when all nine pass, and not applicable counts as passing.`));
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
for (const criterion of misses.length > 0 ? misses : unjudged) {
|
|
333
|
+
const names = criterionLabel(model, criterion.checkId, criterion.criterionId);
|
|
334
|
+
const p = verdictProbability(criterion);
|
|
335
|
+
const item = element("li", `${names.label} (${names.question}): ${VERDICT_TEXT[criterion.verdict]}${p === undefined ? "" : `, P ${fmtP(p)}`}${criterion.confidence === undefined ? "" : `, confidence ${fmtP(criterion.confidence)}`}.`);
|
|
336
|
+
item.dataset.whyCriterion = `${criterion.checkId}.${criterion.criterionId}`;
|
|
337
|
+
why.append(item);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
why.append(el("li", "muted", "Turns cited: none. The judge returns a verdict and probabilities per criterion, with no reference to any turn of the conversation."));
|
|
341
|
+
body.append(why);
|
|
342
|
+
// 4. Check it
|
|
343
|
+
body.append(el("h4", "", "Check it"));
|
|
344
|
+
const check = el("dl", "chk");
|
|
345
|
+
check.dataset.checkIt = conversation.conversationId;
|
|
346
|
+
const row = (label, value) => {
|
|
347
|
+
check.append(element("dt", label), element("dd", value));
|
|
348
|
+
};
|
|
349
|
+
row("Case", conversation.conversationId);
|
|
350
|
+
row("Case record", conversation.sourceCapsuleId ?? "not stated by the reports");
|
|
351
|
+
// A conversation dated by its judged day (no case record date) adds no
|
|
352
|
+
// rows: the page reads exactly as it did before record dating.
|
|
353
|
+
const dated = conversation.dated;
|
|
354
|
+
if (dated.basis !== "judged") {
|
|
355
|
+
row("Dated by", `${dated.stated ?? dated.date}${dated.zone === "not-stated" ? " (timezone not stated)" : ""}: ${BASIS_TEXT[dated.basis]}`);
|
|
356
|
+
check.lastElementChild.setAttribute("data-dated", dated.basis);
|
|
357
|
+
if (dated.importedAt !== undefined)
|
|
358
|
+
row("Imported", dated.importedAt);
|
|
359
|
+
row("Judged and sealed", conversation.judgedDay);
|
|
360
|
+
}
|
|
361
|
+
row("Result capsule", model.capsuleId);
|
|
362
|
+
if (conversation.transcript.state === "verified") {
|
|
363
|
+
row("Transcript record", conversation.transcript.transcriptRecordId);
|
|
364
|
+
row("Case input digest", conversation.transcript.inputDigest);
|
|
365
|
+
}
|
|
366
|
+
row("Judge pin", runInfo.judgePinDigest === undefined
|
|
367
|
+
? "not stated"
|
|
368
|
+
: `${runInfo.judgePinDigest}${runInfo.judgePinRecomputes === true && runInfo.judgePin !== undefined ? ` = ${runInfo.judgePin.modelId}, prompt ${runInfo.judgePin.promptDigest}, axes ${runInfo.judgePin.axesDigest} (recomputed on this page)` : ""}`);
|
|
369
|
+
for (const criterion of conversation.criteria) {
|
|
370
|
+
if (criterion.claim === undefined)
|
|
371
|
+
continue;
|
|
372
|
+
row(`Report: ${criterionLabel(model, criterion.checkId, criterion.criterionId).label}`, `${criterion.reportCapsuleId ?? "not cited"}${criterion.reportRecordId === undefined ? "" : ` (book record ${criterion.reportRecordId})`}`);
|
|
373
|
+
}
|
|
374
|
+
body.append(check);
|
|
375
|
+
body.append(el("p", "small", "To recompute: read each report above from the book, check its digest, and take the AND of the nine verdicts (not applicable passes). To re-judge, run the pinned judge on the case record and compare."));
|
|
376
|
+
return body;
|
|
377
|
+
}
|
|
378
|
+
function renderConversationList(conversations, model, runInfo, key) {
|
|
379
|
+
const list = el("div", "convs");
|
|
380
|
+
list.dataset.conversationsFor = key;
|
|
381
|
+
list.dataset.conversationCount = String(conversations.length);
|
|
382
|
+
if (conversations.length === 0)
|
|
383
|
+
list.append(el("p", "small", "no conversations"));
|
|
384
|
+
for (const conversation of conversations)
|
|
385
|
+
list.append(renderConversation(conversation, model, runInfo));
|
|
386
|
+
return list;
|
|
387
|
+
}
|
|
388
|
+
function drill(label, conversations, model, runInfo, key) {
|
|
389
|
+
const details = el("details", "drill");
|
|
390
|
+
details.dataset.drill = key;
|
|
391
|
+
details.append(el("summary", "", label));
|
|
392
|
+
details.append(renderConversationList(conversations, model, runInfo, key));
|
|
393
|
+
return details;
|
|
394
|
+
}
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// The sheet: header meta, report lines, stamp, tamper note
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
/** The pack and version as one line: both come from the claims' `contract_ref` (see `readOutcomeReportTerms`), or read "not stated". */
|
|
399
|
+
function packLine(model) {
|
|
400
|
+
const { id, version } = model.terms.pack;
|
|
401
|
+
return id === NOT_STATED ? NOT_STATED : `${id} v${version}`;
|
|
402
|
+
}
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
// Dating: which date the calendar and period count a conversation under, and
|
|
405
|
+
// the plain label that says so (see `ConversationDate`)
|
|
406
|
+
// ---------------------------------------------------------------------------
|
|
407
|
+
function span(days) {
|
|
408
|
+
const first = days[0];
|
|
409
|
+
const last = days[days.length - 1];
|
|
410
|
+
if (first === undefined || last === undefined)
|
|
411
|
+
return "unknown";
|
|
412
|
+
return first === last ? first : `${first} – ${last}`;
|
|
413
|
+
}
|
|
414
|
+
const BASIS_TEXT = {
|
|
415
|
+
source: "source says (backfilled record; the source's own timestamp, self-attested)",
|
|
416
|
+
record: "the case record's own timestamp",
|
|
417
|
+
judged: "the day the judgments were sealed (no case record date to read)",
|
|
418
|
+
};
|
|
419
|
+
/** The page's one plain statement of what its dates are, or undefined when every conversation is dated by the day it was judged (the card's behaviour before record dating). */
|
|
420
|
+
function datingNote(model) {
|
|
421
|
+
const { basis, counts, judgedDays, zoneNotStated } = model.dating;
|
|
422
|
+
if (basis === "judged")
|
|
423
|
+
return undefined;
|
|
424
|
+
const sealed = `Sealed ${span(judgedDays)}.`;
|
|
425
|
+
const parts = [];
|
|
426
|
+
if (basis === "source")
|
|
427
|
+
parts.push('Dates are the source\'s own timestamps for backfilled records ("source says"), as given, not when the records were sealed.');
|
|
428
|
+
else if (basis === "record")
|
|
429
|
+
parts.push("Dates are each conversation's own record date (the time its case record states), as given, not when the judgments were sealed.");
|
|
430
|
+
else {
|
|
431
|
+
parts.push("Dates are each conversation's own date, as given, not when the judgments were sealed:");
|
|
432
|
+
if (counts.source > 0)
|
|
433
|
+
parts.push(`${counts.source} backfilled by the source's own timestamp ("source says");`);
|
|
434
|
+
if (counts.record > 0)
|
|
435
|
+
parts.push(`${counts.record} by the case record's own timestamp;`);
|
|
436
|
+
if (counts.judged > 0)
|
|
437
|
+
parts.push(`${counts.judged} with no case record date to read, by the day they were judged.`);
|
|
438
|
+
}
|
|
439
|
+
parts.push(sealed);
|
|
440
|
+
if (counts.source > 0)
|
|
441
|
+
parts.push("A backfilled record's source time is self-attested: carrying it does not make it witnessed.");
|
|
442
|
+
if (zoneNotStated > 0)
|
|
443
|
+
parts.push(`Timezone not stated on ${zoneNotStated} of ${model.totalCount}; no zone is assumed.`);
|
|
444
|
+
return parts.join(" ");
|
|
445
|
+
}
|
|
446
|
+
function renderMeta(host, model) {
|
|
447
|
+
const section = el("section", "");
|
|
448
|
+
section.dataset.section = "meta";
|
|
449
|
+
const row1 = el("div", "ih-row");
|
|
450
|
+
const bill = el("div", "party");
|
|
451
|
+
bill.append(el("div", "k", "Outcomes pack"));
|
|
452
|
+
bill.append(el("div", "v", packLine(model)));
|
|
453
|
+
row1.append(bill);
|
|
454
|
+
const title = el("div", "ih-title");
|
|
455
|
+
title.append(el("h1", "inv-title", "Outcome report"));
|
|
456
|
+
title.append(el("div", "inv-sub", `Terms locked: ${model.terms.pack.locked}`));
|
|
457
|
+
row1.append(title);
|
|
458
|
+
section.append(row1);
|
|
459
|
+
const row2 = el("div", "ih-row");
|
|
460
|
+
const details = el("div", "meta");
|
|
461
|
+
const first = model.days[0]?.date;
|
|
462
|
+
const last = model.days[model.days.length - 1]?.date;
|
|
463
|
+
const metaRow = (label, value) => {
|
|
464
|
+
const line = element("div");
|
|
465
|
+
line.append(element("span", label), element("span", value));
|
|
466
|
+
details.append(line);
|
|
467
|
+
};
|
|
468
|
+
metaRow("Period", first === undefined || last === undefined
|
|
469
|
+
? "no conversations"
|
|
470
|
+
: `${first} – ${last}`);
|
|
471
|
+
const note = datingNote(model);
|
|
472
|
+
if (note !== undefined) {
|
|
473
|
+
metaRow("Sealed", span(model.dating.judgedDays));
|
|
474
|
+
details.lastElementChild.setAttribute("data-sealed", "");
|
|
475
|
+
}
|
|
476
|
+
metaRow("Conversations", fmtCount(model.totalCount));
|
|
477
|
+
metaRow("Result capsule", model.capsuleId);
|
|
478
|
+
row2.append(el("div", "party"), details);
|
|
479
|
+
section.append(row2);
|
|
480
|
+
if (note !== undefined) {
|
|
481
|
+
const label = el("p", "date-note", note);
|
|
482
|
+
label.dataset.dateNote = model.dating.basis;
|
|
483
|
+
section.append(label);
|
|
484
|
+
}
|
|
485
|
+
host.append(section);
|
|
486
|
+
}
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
// Report lines
|
|
489
|
+
// ---------------------------------------------------------------------------
|
|
490
|
+
function renderLines(host, model, runInfo) {
|
|
491
|
+
const section = el("section", "");
|
|
492
|
+
section.dataset.section = "lines";
|
|
493
|
+
const table = document.createElement("table");
|
|
494
|
+
table.className = "lines";
|
|
495
|
+
table.dataset.rows = "report-lines";
|
|
496
|
+
const thead = document.createElement("thead");
|
|
497
|
+
const headRow = document.createElement("tr");
|
|
498
|
+
headRow.append(element("th", "Description"), element("th", "Count"));
|
|
499
|
+
thead.append(headRow);
|
|
500
|
+
table.append(thead);
|
|
501
|
+
const tbody = document.createElement("tbody");
|
|
502
|
+
// held is read from calibration samples, resolved from claim rollups --
|
|
503
|
+
// two independent sources this card shows side by side and never
|
|
504
|
+
// subtracts one from the other.
|
|
505
|
+
const lines = [
|
|
506
|
+
[
|
|
507
|
+
"resolved",
|
|
508
|
+
"Conversations resolved correctly",
|
|
509
|
+
model.resolvedCount,
|
|
510
|
+
fmtShare(model.resolvedCount, model.totalCount, model.options.percentages),
|
|
511
|
+
],
|
|
512
|
+
[
|
|
513
|
+
"missed",
|
|
514
|
+
"Conversations missed",
|
|
515
|
+
model.missedCount,
|
|
516
|
+
fmtShare(model.missedCount, model.totalCount, model.options.percentages),
|
|
517
|
+
],
|
|
518
|
+
[
|
|
519
|
+
"held",
|
|
520
|
+
"Held pending adjudication",
|
|
521
|
+
model.heldCount,
|
|
522
|
+
fmtCount(model.heldCount),
|
|
523
|
+
],
|
|
524
|
+
[
|
|
525
|
+
"added",
|
|
526
|
+
"Added after adjudication",
|
|
527
|
+
model.addedCount,
|
|
528
|
+
fmtCount(model.addedCount),
|
|
529
|
+
],
|
|
530
|
+
[
|
|
531
|
+
"not-applicable",
|
|
532
|
+
"Criterion judgments not applicable",
|
|
533
|
+
model.notApplicableCount,
|
|
534
|
+
fmtCount(model.notApplicableCount),
|
|
535
|
+
],
|
|
536
|
+
];
|
|
537
|
+
for (const [key, label, count, shown] of lines) {
|
|
538
|
+
const tr = document.createElement("tr");
|
|
539
|
+
tr.dataset.line = key;
|
|
540
|
+
tr.dataset.count = String(count);
|
|
541
|
+
const desc = el("td", "desc");
|
|
542
|
+
desc.append(el("b", "", label));
|
|
543
|
+
const behind = key === "resolved"
|
|
544
|
+
? model.conversations.filter((c) => c.verdict === "met")
|
|
545
|
+
: key === "missed"
|
|
546
|
+
? model.conversations.filter((c) => c.verdict !== "met")
|
|
547
|
+
: key === "not-applicable"
|
|
548
|
+
? model.conversations.filter((c) => c.criteria.some((cr) => cr.verdict === "not_applicable"))
|
|
549
|
+
: undefined;
|
|
550
|
+
if (behind !== undefined && behind.length > 0)
|
|
551
|
+
desc.append(drill(`open › ${fmtCount(behind.length)} conversation${behind.length === 1 ? "" : "s"}`, behind, model, runInfo, key));
|
|
552
|
+
tr.append(desc, el("td", "n", shown));
|
|
553
|
+
tbody.append(tr);
|
|
554
|
+
}
|
|
555
|
+
table.append(tbody);
|
|
556
|
+
section.append(table);
|
|
557
|
+
host.append(section);
|
|
558
|
+
const stamps = el("div", "stamps inl");
|
|
559
|
+
const stamp = el("div", "stamp rec", "Verified · recomputed from the sealed bundle");
|
|
560
|
+
stamp.append(el("small", "", "every count and share above is recomputed from result.claims and the records they cite, never read from a stated total"));
|
|
561
|
+
stamps.append(stamp);
|
|
562
|
+
host.append(stamps);
|
|
563
|
+
const tamper = el("p", "tamper-note", 'This card only ever renders a bundle that already verified: there is no interactive "try tampering" control here, because this viewer never shows content first and checks second. Tamper with any claim, record, or checkpoint in the underlying bundle and the whole page — this card included — is replaced by the verification-failed banner, before anything here is drawn.');
|
|
564
|
+
tamper.dataset.tamperNote = "verify-first";
|
|
565
|
+
host.append(tamper);
|
|
566
|
+
}
|
|
567
|
+
// ---------------------------------------------------------------------------
|
|
568
|
+
// Day by day
|
|
569
|
+
// ---------------------------------------------------------------------------
|
|
570
|
+
function renderCalendar(host, model) {
|
|
571
|
+
const section = el("section", "sec");
|
|
572
|
+
section.dataset.section = "calendar";
|
|
573
|
+
section.append(el("h2", "", "Day by day"));
|
|
574
|
+
const note = datingNote(model);
|
|
575
|
+
section.append(el("p", "lede", note === undefined
|
|
576
|
+
? "Resolved conversations per day, cited by this bundle's claims."
|
|
577
|
+
: "Resolved conversations per day, by each conversation's own date, cited by this bundle's claims."));
|
|
578
|
+
if (note !== undefined) {
|
|
579
|
+
const label = el("p", "date-note", note);
|
|
580
|
+
label.dataset.dateNote = model.dating.basis;
|
|
581
|
+
section.append(label);
|
|
582
|
+
}
|
|
583
|
+
const list = el("div", "cal");
|
|
584
|
+
list.dataset.days = String(model.days.length);
|
|
585
|
+
for (const day of model.days) {
|
|
586
|
+
const item = el("div", "day");
|
|
587
|
+
item.dataset.day = day.date;
|
|
588
|
+
item.dataset.resolved = String(day.resolvedCount);
|
|
589
|
+
item.dataset.total = String(day.conversations.length);
|
|
590
|
+
item.append(el("div", "dn", day.date));
|
|
591
|
+
if (note !== undefined) {
|
|
592
|
+
const bases = new Set(day.conversations.map((c) => c.dated.basis));
|
|
593
|
+
if (bases.has("source")) {
|
|
594
|
+
const chip = el("div", "src-says", "source says");
|
|
595
|
+
chip.dataset.sourceSays = String(day.conversations.filter((c) => c.dated.basis === "source").length);
|
|
596
|
+
item.append(chip);
|
|
597
|
+
}
|
|
598
|
+
const notStated = day.conversations.filter((c) => c.dated.zone === "not-stated").length;
|
|
599
|
+
if (notStated > 0) {
|
|
600
|
+
const tz = el("div", "tz", "timezone not stated");
|
|
601
|
+
tz.dataset.tzMarker = "not-stated";
|
|
602
|
+
item.append(tz);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
item.append(el("div", "v", fmtShare(day.resolvedCount, day.conversations.length, model.options.percentages)));
|
|
606
|
+
const mini = el("div", "mini");
|
|
607
|
+
const share = day.conversations.length === 0
|
|
608
|
+
? 0
|
|
609
|
+
: day.resolvedCount / day.conversations.length;
|
|
610
|
+
const fill = element("i");
|
|
611
|
+
fill.style.width = `${Math.round(share * 100)}%`;
|
|
612
|
+
mini.append(fill);
|
|
613
|
+
item.append(mini);
|
|
614
|
+
if (note !== undefined) {
|
|
615
|
+
const sealed = el("div", "sealed", `sealed ${span(day.judgedDays)}`);
|
|
616
|
+
sealed.dataset.sealed = day.judgedDays.join(" ");
|
|
617
|
+
item.append(sealed);
|
|
618
|
+
}
|
|
619
|
+
if (day.policyChanged !== null) {
|
|
620
|
+
const flag = el("div", "flag", "policy changed");
|
|
621
|
+
flag.dataset.policyChanged = day.policyChanged;
|
|
622
|
+
item.append(flag);
|
|
623
|
+
}
|
|
624
|
+
list.append(item);
|
|
625
|
+
}
|
|
626
|
+
section.append(list);
|
|
627
|
+
host.append(section);
|
|
628
|
+
}
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// Human spot checks
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
function renderHumanChecks(host, calibration) {
|
|
633
|
+
const section = el("section", "sec");
|
|
634
|
+
section.dataset.section = "human-checks";
|
|
635
|
+
section.append(el("h2", "", "Human spot checks"));
|
|
636
|
+
section.append(el("p", "lede", "Weekly blind re-grades by a person. Disagreements are held."));
|
|
637
|
+
if (calibration.length === 0) {
|
|
638
|
+
section.append(el("p", "", "no calibration-summary/v1 records cited"));
|
|
639
|
+
host.append(section);
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const table = document.createElement("table");
|
|
643
|
+
table.className = "hct";
|
|
644
|
+
table.dataset.weeks = String(calibration.length);
|
|
645
|
+
const thead = document.createElement("thead");
|
|
646
|
+
const headRow = document.createElement("tr");
|
|
647
|
+
headRow.append(element("th", "Week"), el("th", "n", "Agreed"), el("th", "n", "Held"), el("th", "n", "Added"));
|
|
648
|
+
thead.append(headRow);
|
|
649
|
+
table.append(thead);
|
|
650
|
+
const tbody = document.createElement("tbody");
|
|
651
|
+
for (const week of calibration) {
|
|
652
|
+
const tr = document.createElement("tr");
|
|
653
|
+
tr.dataset.week = week.weekId;
|
|
654
|
+
tr.dataset.k = String(week.k);
|
|
655
|
+
tr.dataset.n = String(week.n);
|
|
656
|
+
tr.append(el("td", "", week.weekId));
|
|
657
|
+
tr.append(el("td", "n", `${week.k} of ${week.n}`));
|
|
658
|
+
const held = week.sample.filter((s) => s.aiVerdict === "met" && s.humanVerdict !== "met").length;
|
|
659
|
+
const added = week.sample.filter((s) => s.aiVerdict !== "met" && s.humanVerdict === "met").length;
|
|
660
|
+
const heldCell = el("td", "n", String(held));
|
|
661
|
+
heldCell.dataset.held = String(held);
|
|
662
|
+
const addedCell = el("td", "n", String(added));
|
|
663
|
+
addedCell.dataset.added = String(added);
|
|
664
|
+
tr.append(heldCell, addedCell);
|
|
665
|
+
tbody.append(tr);
|
|
666
|
+
}
|
|
667
|
+
table.append(tbody);
|
|
668
|
+
section.append(table);
|
|
669
|
+
host.append(section);
|
|
670
|
+
}
|
|
671
|
+
// ---------------------------------------------------------------------------
|
|
672
|
+
// Why conversations missed
|
|
673
|
+
// ---------------------------------------------------------------------------
|
|
674
|
+
function renderWhyMissed(host, model, runInfo) {
|
|
675
|
+
const section = el("section", "sec");
|
|
676
|
+
section.dataset.section = "why-missed";
|
|
677
|
+
section.append(el("h2", "", "Why conversations missed"));
|
|
678
|
+
const bars = el("div", "bars");
|
|
679
|
+
bars.dataset.checkBars = "true";
|
|
680
|
+
for (const check of model.terms.checks) {
|
|
681
|
+
const count = model.checkPassCounts.get(check.id) ?? 0;
|
|
682
|
+
const item = el("div", "bar");
|
|
683
|
+
item.dataset.checkId = check.id;
|
|
684
|
+
item.dataset.passCount = String(count);
|
|
685
|
+
const nm = el("div", "nm", check.question);
|
|
686
|
+
const track = el("div", "track");
|
|
687
|
+
const share = model.totalCount === 0 ? 0 : count / model.totalCount;
|
|
688
|
+
const fill = element("i");
|
|
689
|
+
fill.style.width = `${Math.round(share * 100)}%`;
|
|
690
|
+
track.append(fill);
|
|
691
|
+
const num = el("div", "num", fmtShare(count, model.totalCount, model.options.percentages));
|
|
692
|
+
item.append(nm, track, num);
|
|
693
|
+
bars.append(item);
|
|
694
|
+
}
|
|
695
|
+
section.append(bars);
|
|
696
|
+
const reasons = el("div", "reasons");
|
|
697
|
+
reasons.dataset.reasons = String(model.missedReasons.length);
|
|
698
|
+
for (const group of model.missedReasons) {
|
|
699
|
+
const item = el("details", "rsn-d");
|
|
700
|
+
item.dataset.reasonKey = group.key;
|
|
701
|
+
item.dataset.reasonStructured = String(group.structured);
|
|
702
|
+
item.dataset.count = String(group.conversations.length);
|
|
703
|
+
const row = el("summary", "rsn");
|
|
704
|
+
row.append(element("span", group.label), el("span", "c", `${fmtCount(group.conversations.length)} · see them ›`));
|
|
705
|
+
item.append(row);
|
|
706
|
+
item.append(renderConversationList(group.conversations, model, runInfo, `reason:${group.key}`));
|
|
707
|
+
reasons.append(item);
|
|
708
|
+
}
|
|
709
|
+
section.append(reasons);
|
|
710
|
+
host.append(section);
|
|
711
|
+
}
|
|
712
|
+
// ---------------------------------------------------------------------------
|
|
713
|
+
// The terms
|
|
714
|
+
// ---------------------------------------------------------------------------
|
|
715
|
+
function renderTerms(host, model) {
|
|
716
|
+
const section = el("section", "sec");
|
|
717
|
+
section.dataset.section = "terms";
|
|
718
|
+
section.append(el("h2", "", "The terms: what counts as resolved"));
|
|
719
|
+
const lede = el("p", "lede", `Agreed with the counterparty: ${model.terms.pack.agreement}. Locked: ${model.terms.pack.locked}. Every term below is read from the sealed claims and reports in this bundle, or marked "${NOT_STATED}".`);
|
|
720
|
+
lede.dataset.termsSource = "bundle";
|
|
721
|
+
section.append(lede);
|
|
722
|
+
const pack = el("div", "pack");
|
|
723
|
+
const packField = (label, value) => {
|
|
724
|
+
const box = el("div", "pk");
|
|
725
|
+
box.append(el("div", "k", label), el("div", "v", value));
|
|
726
|
+
pack.append(box);
|
|
727
|
+
};
|
|
728
|
+
packField("Outcomes pack", model.terms.pack.id);
|
|
729
|
+
packField("Version", model.terms.pack.version === NOT_STATED
|
|
730
|
+
? NOT_STATED
|
|
731
|
+
: `${model.terms.pack.version} (from the claims' contract_ref)`);
|
|
732
|
+
packField("Locked", model.terms.pack.locked);
|
|
733
|
+
section.append(pack);
|
|
734
|
+
const outcome = el("div", "outcome");
|
|
735
|
+
const outcomeIcon = el("div", "oic");
|
|
736
|
+
const target = icon("target");
|
|
737
|
+
if (target !== undefined)
|
|
738
|
+
outcomeIcon.append(target);
|
|
739
|
+
outcome.append(outcomeIcon);
|
|
740
|
+
const outcomeBody = element("div");
|
|
741
|
+
outcomeBody.append(el("div", "t", "The outcome"));
|
|
742
|
+
outcomeBody.append(el("div", "v", model.terms.pack.outcomeStatement));
|
|
743
|
+
outcome.append(outcomeBody);
|
|
744
|
+
section.append(outcome);
|
|
745
|
+
const checks = el("div", "checks");
|
|
746
|
+
checks.dataset.checks = String(model.terms.checks.length);
|
|
747
|
+
for (const check of model.terms.checks) {
|
|
748
|
+
const card = el("section", "check");
|
|
749
|
+
card.dataset.checkId = check.id;
|
|
750
|
+
const checkIcon = el("div", "ic");
|
|
751
|
+
const drawn = icon(check.icon);
|
|
752
|
+
if (drawn !== undefined)
|
|
753
|
+
checkIcon.append(drawn);
|
|
754
|
+
card.append(checkIcon);
|
|
755
|
+
card.append(el("div", "q", check.question));
|
|
756
|
+
card.append(el("div", "d", check.description));
|
|
757
|
+
const criteria = el("ul", "subs");
|
|
758
|
+
for (const criterion of check.criteria) {
|
|
759
|
+
const ref = `${check.id}.${criterion.id}`;
|
|
760
|
+
const tier = model.criterionTiers.get(ref) ?? "unknown";
|
|
761
|
+
const item = element("li");
|
|
762
|
+
item.dataset.criterionId = criterion.id;
|
|
763
|
+
item.dataset.tier = tier;
|
|
764
|
+
const wording = model.criterionWording.get(ref) ?? {
|
|
765
|
+
text: criterion.text,
|
|
766
|
+
source: "unstated",
|
|
767
|
+
};
|
|
768
|
+
item.dataset.wording = wording.source;
|
|
769
|
+
const label = el("b", "", criterion.label);
|
|
770
|
+
const termsBadge = editedBadge(model, ref);
|
|
771
|
+
if (termsBadge !== undefined)
|
|
772
|
+
label.append(termsBadge);
|
|
773
|
+
item.append(label);
|
|
774
|
+
item.append(element("span", `${wording.text} (${TIER_LABEL[tier]})`));
|
|
775
|
+
if (wording.source !== "sealed")
|
|
776
|
+
item.append(el("small", "wsrc", WORDING_FALLBACK_LABEL[wording.source]));
|
|
777
|
+
criteria.append(item);
|
|
778
|
+
}
|
|
779
|
+
card.append(criteria);
|
|
780
|
+
checks.append(card);
|
|
781
|
+
}
|
|
782
|
+
section.append(checks);
|
|
783
|
+
const note = el("p", "small", wordingNote(model));
|
|
784
|
+
note.dataset.wordingNote = "true";
|
|
785
|
+
section.append(note);
|
|
786
|
+
section.append(el("div", "rule", model.terms.pack.resolutionRule));
|
|
787
|
+
host.append(section);
|
|
788
|
+
}
|
|
789
|
+
// ---------------------------------------------------------------------------
|
|
790
|
+
// What ran
|
|
791
|
+
// ---------------------------------------------------------------------------
|
|
792
|
+
function renderWhatRan(host, runInfo) {
|
|
793
|
+
const section = el("section", "sec");
|
|
794
|
+
section.dataset.section = "what-ran";
|
|
795
|
+
section.append(el("h2", "", "What ran"));
|
|
796
|
+
const ran = el("div", "ran");
|
|
797
|
+
ran.dataset.pinned = String(runInfo.pinned);
|
|
798
|
+
if (runInfo.judgePinRecomputes !== undefined)
|
|
799
|
+
ran.dataset.pinRecomputes = String(runInfo.judgePinRecomputes);
|
|
800
|
+
const box = (label, value, key) => {
|
|
801
|
+
const item = el("div", "it");
|
|
802
|
+
item.dataset.ran = key;
|
|
803
|
+
item.append(el("div", "k", label));
|
|
804
|
+
item.append(el("div", "v", value));
|
|
805
|
+
ran.append(item);
|
|
806
|
+
};
|
|
807
|
+
// The pin's components are drawn only when they recompute to the sealed
|
|
808
|
+
// digest (verifyJudgePin); otherwise they are "not stated", never shown
|
|
809
|
+
// beside a digest they do not produce.
|
|
810
|
+
const pin = runInfo.judgePinRecomputes === true ? runInfo.judgePin : undefined;
|
|
811
|
+
box("Contract", runInfo.contract ?? "not stated", "contract");
|
|
812
|
+
box("Judge model", pin === undefined
|
|
813
|
+
? "not stated"
|
|
814
|
+
: pin.modelVersion === undefined
|
|
815
|
+
? pin.modelId
|
|
816
|
+
: `${pin.modelId} ${pin.modelVersion}`, "judge-model");
|
|
817
|
+
box("Judge pin digest", runInfo.judgePinDigest === undefined
|
|
818
|
+
? "not stated"
|
|
819
|
+
: pin === undefined
|
|
820
|
+
? runInfo.judgePinDigest
|
|
821
|
+
: `${runInfo.judgePinDigest} (recomputed from the model, prompt, axes and sampling params shown here)`, "judge-pin-digest");
|
|
822
|
+
box("Judge prompt digest", pin?.promptDigest ?? "not stated", "judge-prompt-digest");
|
|
823
|
+
box("Judge axes digest", pin?.axesDigest ?? "not stated", "judge-axes-digest");
|
|
824
|
+
box("Agent / test suite / human-check protocol", "not stated — this evaluation-report/v1 shape carries none of these fields", "agent");
|
|
825
|
+
section.append(ran);
|
|
826
|
+
if (runInfo.judgePinRecomputes === false) {
|
|
827
|
+
const warning = el("p", "", "the judge pin components the reports carry do not recompute to the sealed judge pin digest — they are not shown");
|
|
828
|
+
warning.dataset.pinComponentsMismatch = "true";
|
|
829
|
+
section.append(warning);
|
|
830
|
+
}
|
|
831
|
+
const lock = el("div", "lock", "The judge pin digest covers model id, judge prompt and axes, and sampling params (capsulectl judge pin). It does not cover the human-check protocol name: this bundle seals no method lock, and this card never implies one exists.");
|
|
832
|
+
section.append(lock);
|
|
833
|
+
if (!runInfo.pinned) {
|
|
834
|
+
const warning = el("p", "", "what ran varies across the cited reports — the values above are drawn only when every one agrees, and are omitted ('not stated') here because they do not");
|
|
835
|
+
warning.dataset.pinMismatch = "true";
|
|
836
|
+
section.append(warning);
|
|
837
|
+
}
|
|
838
|
+
host.append(section);
|
|
839
|
+
}
|
|
840
|
+
// ---------------------------------------------------------------------------
|
|
841
|
+
// Verification details
|
|
842
|
+
// ---------------------------------------------------------------------------
|
|
843
|
+
function renderVerificationDetails(host, bundle, verified) {
|
|
844
|
+
const section = el("section", "sec");
|
|
845
|
+
section.dataset.section = "verification-details";
|
|
846
|
+
section.append(el("h2", "", "Verification details"));
|
|
847
|
+
section.append(el("p", "lede", "Repeat offline with the open-source verifier."));
|
|
848
|
+
const model = buildVerificationPageModel(bundle, verified);
|
|
849
|
+
const tech = el("div", "tech");
|
|
850
|
+
const field = (label, value) => {
|
|
851
|
+
tech.append(el("div", "k", label), el("div", "v", value));
|
|
852
|
+
};
|
|
853
|
+
field("Bundle digest", model.bundleDigest ?? "uncomputable");
|
|
854
|
+
field("Checkpoint root", model.checkpointRoot ?? "absent");
|
|
855
|
+
field("Checkpoint size", String(model.checkpointSize ?? "absent"));
|
|
856
|
+
section.append(tech);
|
|
857
|
+
const note = el("p", "note", "This bundle's own checkpoint — an append-only Merkle Mountain Range over the sealed records — not a month-level RFC 6962 tree computed in the browser. Repeat this check offline with the open-source verifier.");
|
|
858
|
+
note.dataset.checkpointKind = "mmr";
|
|
859
|
+
section.append(note);
|
|
860
|
+
host.append(section);
|
|
861
|
+
}
|
|
862
|
+
export async function renderOutcomeReportPage(result, options, bundle, verified, root) {
|
|
863
|
+
const model = buildOutcomeReportModel(result, options);
|
|
864
|
+
const runInfo = await verifyJudgePin(model.runInfo);
|
|
865
|
+
const wrapper = el("section", "oi");
|
|
866
|
+
wrapper.dataset.page = "outcome-report";
|
|
867
|
+
const style = document.createElement("style");
|
|
868
|
+
style.textContent = OUTCOME_REPORT_CSS;
|
|
869
|
+
wrapper.append(style);
|
|
870
|
+
renderProducerNote(wrapper, model);
|
|
871
|
+
const sheet = el("div", "sheet");
|
|
872
|
+
const pad = el("div", "pad");
|
|
873
|
+
renderMeta(pad, model);
|
|
874
|
+
renderLines(pad, model, runInfo);
|
|
875
|
+
sheet.append(pad);
|
|
876
|
+
wrapper.append(sheet);
|
|
877
|
+
renderCalendar(wrapper, model);
|
|
878
|
+
renderHumanChecks(wrapper, model.calibration);
|
|
879
|
+
renderWhyMissed(wrapper, model, runInfo);
|
|
880
|
+
renderTerms(wrapper, model);
|
|
881
|
+
renderWhatRan(wrapper, runInfo);
|
|
882
|
+
renderVerificationDetails(wrapper, bundle, verified);
|
|
883
|
+
root.append(wrapper);
|
|
884
|
+
}
|
|
885
|
+
//# sourceMappingURL=outcome-report-view.js.map
|