@kontourai/survey 6.1.0 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/src/calibration.d.ts +15 -0
  2. package/dist/src/calibration.js +11 -0
  3. package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
  4. package/dist/src/canonical-reviewed-trust-input.js +45 -1
  5. package/dist/src/console/review-console-server.js +22 -4
  6. package/dist/src/decision-carry-forward.d.ts +157 -0
  7. package/dist/src/decision-carry-forward.js +295 -0
  8. package/dist/src/extraction-envelope.d.ts +4 -3
  9. package/dist/src/extraction-envelope.js +3 -3
  10. package/dist/src/field-states.d.ts +100 -0
  11. package/dist/src/field-states.js +169 -0
  12. package/dist/src/index.d.ts +6 -4
  13. package/dist/src/index.js +3 -1
  14. package/dist/src/mcp/review-mcp.js +173 -45
  15. package/dist/src/review-resource.d.ts +36 -0
  16. package/dist/src/review-session-file.d.ts +7 -0
  17. package/dist/src/review-workbench/audit-rows.d.ts +1 -1
  18. package/dist/src/review-workbench/audit-rows.js +1 -0
  19. package/dist/src/review-workbench/audit-sample.d.ts +32 -0
  20. package/dist/src/review-workbench/audit-sample.js +53 -0
  21. package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
  22. package/dist/src/review-workbench/extraction-inspector.js +1 -1
  23. package/dist/src/review-workbench/field-state-view.d.ts +15 -0
  24. package/dist/src/review-workbench/field-state-view.js +84 -0
  25. package/dist/src/review-workbench/queue-binding.d.ts +45 -2
  26. package/dist/src/review-workbench/queue-binding.js +45 -0
  27. package/dist/src/review-workbench/review-presentation.d.ts +23 -0
  28. package/dist/src/review-workbench/review-presentation.js +51 -10
  29. package/dist/src/review-workbench/review-queue-session.d.ts +93 -5
  30. package/dist/src/review-workbench/review-queue-session.js +147 -11
  31. package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
  32. package/dist/src/review-workbench/review-session-replay.js +49 -1
  33. package/dist/src/review-workbench/review-workbench-css.generated.js +126 -0
  34. package/dist/src/review-workbench/review-workbench-element.d.ts +10 -0
  35. package/dist/src/review-workbench/review-workbench-element.js +21 -3
  36. package/dist/src/review-workbench/review-workbench.css +126 -0
  37. package/dist/src/review-workbench/review-workbench.d.ts +40 -8
  38. package/dist/src/review-workbench/review-workbench.js +192 -71
  39. package/dist/src/review-workbench/review-workbench.standalone.css +126 -0
  40. package/dist/src/review-workbench/server-review-session.d.ts +18 -0
  41. package/dist/src/review-workbench/server-review-session.js +20 -2
  42. package/dist/src/surface-reviewed-extraction.d.ts +23 -0
  43. package/dist/src/surface-reviewed-extraction.js +64 -1
  44. package/dist/src/to-surface.js +32 -3
  45. package/package.json +1 -1
@@ -1,32 +1,38 @@
1
1
  import { canonicalJson } from "./canonical.js";
2
2
  import { assertReviewResolutionConsistency } from "../producer-discipline.js";
3
- import { candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
3
+ import { candidateForDecision, conflictSelectionIssue, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, unchosenProposedCandidates, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, isScoreBlind, assertReviewSessionConditions, sessionConditionsStamp, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
4
4
  import { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
5
5
  import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
6
6
  import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
7
7
  import { findSoleCandidateById, reviewResourceApiVersion, } from "../review-resource.js";
8
8
  import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
9
- import { candidateVerificationNotes, excludedProposalsSentence, humanizeIdentifier } from "./review-presentation.js";
9
+ import { attestReviewQueueExtraction } from "./queue-binding.js";
10
+ import { candidateVerificationNotes, excludedProposalsSentence, excludedProposalsUnreadableSentence } from "./review-presentation.js";
10
11
  import { editedValueFromEditorText, isIsoCalendarDate, parsePlainDecimal } from "./edited-value.js";
12
+ import { renderFieldStatesHtml } from "./field-state-view.js";
11
13
  import { createAuditFactTrace, } from "./audit-rows.js";
12
14
  export { reviewAuditRowKeys } from "./audit-rows.js";
13
- export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
15
+ export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, attestReviewQueueExtraction, bindReviewQueue, reviewItemCarriesExtractionBinding, unverifiedExtractionQueueSentence, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
14
16
  export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
15
- export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
17
+ export { renderFieldStatesHtml } from "./field-state-view.js";
18
+ export { drawRandomAuditSample, openRandomAuditSession, } from "./audit-sample.js";
19
+ export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, conflictSelectionIssue, unchosenProposedCandidates, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, isScoreBlind, assertReviewSessionConditions, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
16
20
  export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
17
21
  export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
18
22
  export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
19
23
  export { checkEditedValueForItem, editedValueFromEditorText, } from "./edited-value.js";
20
24
  export { assertReviewDecisionModeAllows, DecisionModeViolationError, validateReviewDecisionMode, } from "./producer-decision-mode.js";
21
- export function buildReviewDecision(state) {
25
+ export function buildReviewDecision(state, options = {}) {
22
26
  if (!state.decision) {
23
27
  return undefined;
24
28
  }
25
29
  const definition = workbenchDecisionDefinitions[state.decision];
30
+ assertReviewSessionConditions(state);
26
31
  if (state.decision === "could-not-confirm" && !state.note.trim()) {
27
32
  throw new Error("Could not confirm requires a non-empty reason.");
28
33
  }
29
- const candidate = candidateForDecision(state.item, state.decision);
34
+ const candidate = candidateForDecision(state.item, state.decision, state.selectedCandidateId);
35
+ const unchosen = unchosenProposedCandidates(state.item, state.decision, state.selectedCandidateId);
30
36
  // A decision that selects no candidate (a value-neutral decision on a
31
37
  // conflict) records no candidate id and no candidate's projection hints.
32
38
  const selectsNone = decisionSelectsNoCandidate(state.item, state.decision);
@@ -36,7 +42,7 @@ export function buildReviewDecision(state) {
36
42
  ?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
37
43
  };
38
44
  const candidateProjection = selectsNone ? undefined : candidate.projection;
39
- const authorizing = buildDecisionCardAuthorizing(state);
45
+ const authorizing = buildDecisionCardAuthorizing(state, options.presentationAdapter);
40
46
  const reviewDecision = {
41
47
  apiVersion: reviewResourceApiVersion,
42
48
  kind: "ReviewDecision",
@@ -66,6 +72,12 @@ export function buildReviewDecision(state) {
66
72
  ...(state.decision === "accept-proposed" && state.editedValue !== undefined
67
73
  ? { editedValue: state.editedValue }
68
74
  : {}),
75
+ // A choice between conflicting values records the rivals it passed over.
76
+ ...(unchosen.length ? { unselectedCandidateIds: unchosen.map((entry) => entry.id) } : {}),
77
+ // The session's conditions, never the reviewer's: a score-blind session
78
+ // records that the decision was made without scores on screen.
79
+ ...(state.presentation !== undefined ? { presentation: { scoreBlind: state.presentation.scoreBlind } } : {}),
80
+ ...(state.sampling !== undefined ? { sampling: { ...state.sampling } } : {}),
69
81
  },
70
82
  status: {
71
83
  ...(candidateProjection?.claimId ? { appliedToClaimIds: [candidateProjection.claimId] } : {}),
@@ -91,12 +103,11 @@ const DECISION_CARD_PROMPT_REF = "review-workbench/decision-card@v1";
91
103
  * On validation failure, emits a console warning and returns undefined so the
92
104
  * outcome is recorded without authorizing (transparency-gap-not-blocker per ADR 0004).
93
105
  */
94
- function buildDecisionCardAuthorizing(state) {
106
+ function buildDecisionCardAuthorizing(state, adapter) {
95
107
  if (!state.decision) {
96
108
  return undefined;
97
109
  }
98
- const targetLabel = humanizeIdentifier(state.item.spec.target);
99
- const renderedPrompt = decisionCardRenderedPrompt(state, targetLabel);
110
+ const renderedPrompt = decisionCardRenderedPrompt(state, buildReviewItemPresentation(state.item, adapter), adapter);
100
111
  // Reviewer note present means the reviewer also typed a rationale — "typed".
101
112
  // Control-only affirmation (no note) is "affirmed-control".
102
113
  const action = state.note?.trim() ? "typed" : "affirmed-control";
@@ -121,35 +132,48 @@ function buildDecisionCardAuthorizing(state) {
121
132
  }
122
133
  }
123
134
  /**
124
- * Derives the exact decision prompt rendered on the workbench decision card
125
- * for a given review item and decision.
135
+ * Renders the decision card's prompt for a review item and decision.
136
+ *
137
+ * It is rebuilt from the item when the decision is built, not captured from
138
+ * what a reviewer's screen showed: it records what the card states for this
139
+ * item, with the same presentation adapter, target label and value text.
126
140
  *
127
141
  * Format mirrors the review question shown in the workbench header:
128
142
  * "For {target}, decide whether {proposed} should replace {current}."
129
143
  * followed by the selected decision label so the block is self-contained.
130
144
  */
131
- function decisionCardRenderedPrompt(state, targetLabel) {
132
- // The card shows proposals excluded at import; the recorded prompt must say
133
- // the reviewer was told about them.
134
- const excluded = excludedProposalsSentence(buildReviewItemPresentation(state.item).excludedProposals);
135
- // Likewise the verifier records the card showed, read against the same value.
136
- const verification = candidateVerificationNotes(state.item, state.decision === "accept-proposed" ? state.editedValue : undefined).map((note) => note.sentence);
137
- return [decisionCardBasePrompt(state, targetLabel), ...(excluded ? [excluded] : []), ...verification].join(" ");
138
- }
139
- function decisionCardBasePrompt(state, targetLabel) {
145
+ function decisionCardRenderedPrompt(state, presentation, adapter) {
146
+ // The card states proposals excluded at import, and stored ones it cannot
147
+ // show; so does the prompt.
148
+ const excluded = excludedProposalsSentence(presentation.excludedProposals);
149
+ const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
150
+ // Likewise the verifier records the card shows, read against the same value.
151
+ // A score-blind card shows no verifier result, so the recorded prompt names none.
152
+ const verification = isScoreBlind(state) ? [] : candidateVerificationNotes(state.item, state.decision === "accept-proposed" ? state.editedValue : undefined).map((note) => note.sentence);
153
+ return [decisionCardBasePrompt(state, presentation.targetLabel, adapter), ...(excluded ? [excluded] : []), ...(unreadable ? [unreadable] : []), ...verification].join(" ");
154
+ }
155
+ function decisionCardBasePrompt(state, targetLabel, adapter) {
156
+ const valueText = (candidate) => buildReviewCandidatePresentation(state.item, candidate, adapter, targetLabel).valueText;
140
157
  const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
141
158
  const proposedCandidates = state.item.spec.candidates.filter((c) => c.role === "proposed");
142
- // A conflict card lists every proposed value and offers reject-all or could
143
- // not confirm; the prompt states the same, naming no single value.
159
+ // A conflict card lists every proposed value and offers a choice of one,
160
+ // reject-all or could not confirm; the prompt states the same: every value,
161
+ // and for a choice, which value was chosen and which were not.
144
162
  if (proposedCandidates.length > 1) {
163
+ const values = `For ${targetLabel}, ${proposedCandidates.length} different values were proposed: ${proposedCandidates.map(valueText).join(", ")}.`;
164
+ if (state.decision === "select-proposed") {
165
+ const chosen = proposedCandidates.find((c) => c.id === state.selectedCandidateId);
166
+ const unchosen = proposedCandidates.filter((c) => c.id !== state.selectedCandidateId);
167
+ return `${values} Selected decision: ${workbenchDecisionDefinitions[state.decision].label}: ${chosen ? valueText(chosen) : formatValue("")} (not chosen: ${unchosen.map(valueText).join(", ")}).`;
168
+ }
145
169
  const decisionLabel = state.decision === "reject-proposed"
146
170
  ? "Reject all values"
147
171
  : state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
148
- return `For ${targetLabel}, ${proposedCandidates.length} different values were proposed: ${proposedCandidates.map((c) => formatValue(c.value)).join(", ")}. Selected decision: ${decisionLabel}.`;
172
+ return `${values} Selected decision: ${decisionLabel}.`;
149
173
  }
150
174
  const proposedCandidate = proposedCandidates[0];
151
- const currentValue = formatValue(currentCandidate?.value ?? "");
152
- const proposedValue = formatValue(proposedCandidate?.value ?? "");
175
+ const currentValue = currentCandidate ? valueText(currentCandidate) : formatValue("");
176
+ const proposedValue = proposedCandidate ? valueText(proposedCandidate) : formatValue("");
153
177
  const decisionLabel = state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
154
178
  return `For ${targetLabel}, decide whether ${proposedValue} should replace ${currentValue}. Selected decision: ${decisionLabel}.`;
155
179
  }
@@ -161,7 +185,7 @@ export class ReviewApplyActionMappingError extends Error {
161
185
  this.issues = issues;
162
186
  }
163
187
  }
164
- export function buildReviewDecisionsFromSession(session) {
188
+ export function buildReviewDecisionsFromSession(session, options = {}) {
165
189
  return session.items.flatMap((item) => {
166
190
  const decision = session.decisionsByItemName[item.metadata.name];
167
191
  if (!decision) {
@@ -170,7 +194,7 @@ export function buildReviewDecisionsFromSession(session) {
170
194
  const reviewDecision = buildReviewDecision(currentReviewWorkbenchState({
171
195
  ...session,
172
196
  activeItemName: item.metadata.name,
173
- }));
197
+ }), options);
174
198
  return reviewDecision ? [reviewDecision] : [];
175
199
  });
176
200
  }
@@ -291,7 +315,7 @@ function matchingSelectedCandidate(item, result) {
291
315
  function structuralEqual(left, right) {
292
316
  return canonicalJson(left) === canonicalJson(right);
293
317
  }
294
- export function buildReviewWorkbenchResultsFromSession(session) {
318
+ export function buildReviewWorkbenchResultsFromSession(session, options = {}) {
295
319
  return session.items.flatMap((item) => {
296
320
  const decision = session.decisionsByItemName[item.metadata.name];
297
321
  if (!decision) {
@@ -301,7 +325,7 @@ export function buildReviewWorkbenchResultsFromSession(session) {
301
325
  ...session,
302
326
  activeItemName: item.metadata.name,
303
327
  });
304
- const reviewDecision = buildReviewDecision(state);
328
+ const reviewDecision = buildReviewDecision(state, options);
305
329
  if (!reviewDecision) {
306
330
  return [];
307
331
  }
@@ -315,11 +339,12 @@ export function buildReviewWorkbenchResultsFromSession(session) {
315
339
  rationale: reviewDecision.spec.rationale,
316
340
  }];
317
341
  }
318
- const selectedCandidate = candidateForDecision(item, decision);
342
+ const selectedCandidateId = session.selectedCandidateIdsByItemName?.[item.metadata.name];
343
+ const selectedCandidate = candidateForDecision(item, decision, selectedCandidateId);
319
344
  const editedValue = decision === "accept-proposed"
320
345
  ? session.editedValuesByItemName?.[item.metadata.name]
321
346
  : undefined;
322
- const effectiveValue = effectiveValueForDecision(item, decision, editedValue);
347
+ const effectiveValue = effectiveValueForDecision(item, decision, editedValue, selectedCandidateId);
323
348
  return [{
324
349
  reviewItemName: item.metadata.name,
325
350
  decision,
@@ -338,12 +363,12 @@ export function buildReviewWorkbenchResultsFromSession(session) {
338
363
  }];
339
364
  });
340
365
  }
341
- export function buildReviewWorkbenchSessionExport(session, events = buildReviewSessionEvents(session)) {
366
+ export function buildReviewWorkbenchSessionExport(session, events = buildReviewSessionEvents(session), options = {}) {
342
367
  return {
343
368
  session: buildReviewSessionResource(session, events),
344
369
  events,
345
- decisions: buildReviewDecisionsFromSession(session),
346
- results: buildReviewWorkbenchResultsFromSession(session),
370
+ decisions: buildReviewDecisionsFromSession(session, options),
371
+ results: buildReviewWorkbenchResultsFromSession(session, options),
347
372
  };
348
373
  }
349
374
  export function replayReviewSessionEventsForSnapshot(snapshot, events) {
@@ -353,9 +378,9 @@ export function replayReviewSessionEventsForSnapshot(snapshot, events) {
353
378
  }
354
379
  return replayReviewSessionEvents(snapshot, events);
355
380
  }
356
- export function buildReviewWorkbenchSessionExportForSnapshot(snapshot, events) {
381
+ export function buildReviewWorkbenchSessionExportForSnapshot(snapshot, events, options = {}) {
357
382
  const replayedSession = replayReviewSessionEventsForSnapshot(snapshot, events);
358
- return buildReviewWorkbenchSessionExport(replayedSession, events);
383
+ return buildReviewWorkbenchSessionExport(replayedSession, events, options);
359
384
  }
360
385
  export function deriveReviewSessionApplyResultForSnapshot(options) {
361
386
  const requiredResolvedItems = options.requiredResolvedItems ?? "none";
@@ -371,7 +396,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
371
396
  }
372
397
  const warnings = reviewSessionReplayWarningsForSnapshot(options.snapshot, options.events);
373
398
  const replayedSession = replayReviewSessionEvents(options.snapshot, options.events);
374
- const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events);
399
+ const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events, { presentationAdapter: options.presentationAdapter });
375
400
  const resolvedItemNames = new Set(sessionExport.results.map((result) => result.reviewItemName));
376
401
  const unresolvedItemNames = options.snapshot.items
377
402
  .map((item) => item.metadata.name)
@@ -498,7 +523,7 @@ export function createPersistentReviewSessionEventStore(options) {
498
523
  }
499
524
  export function renderReviewWorkbenchHtml(state, _events, options = {}) {
500
525
  const session = queueSessionFromStartState(state);
501
- return renderReviewQueueSessionHtml(session, options.presentationAdapter, normalizeReviewQueueView(options.queueView));
526
+ return renderReviewQueueSessionHtml(session, options.presentationAdapter, normalizeReviewQueueView(options.queueView), attestReviewQueueExtraction(session.items, options.extractionImport), options.fieldStates);
502
527
  }
503
528
  /**
504
529
  * The visual/decision state of a field card, derived from the reviewer's local
@@ -506,7 +531,7 @@ export function renderReviewWorkbenchHtml(state, _events, options = {}) {
506
531
  * candidate set (e.g. a pre-decided item seeded by the host) as already-kept.
507
532
  */
508
533
  function fieldCardState(item, decision) {
509
- if (decision === "accept-proposed")
534
+ if (decision === "accept-proposed" || decision === "select-proposed")
510
535
  return "accepted";
511
536
  if (decision === "keep-current")
512
537
  return "kept";
@@ -528,7 +553,9 @@ function fieldCardState(item, decision) {
528
553
  */
529
554
  function chipLabel(state, hasCurrentValue, conflictingValues) {
530
555
  switch (state) {
531
- case "accepted": return "Accepted";
556
+ // On a conflict the only accepting decision is a choice of one value, and
557
+ // the chip says it was a choice, never a plain accept.
558
+ case "accepted": return conflictingValues ? `Chose 1 of ${conflictingValues} values` : "Accepted";
532
559
  case "kept": return hasCurrentValue ? "Kept current" : conflictingValues ? "All values rejected" : "Left unset";
533
560
  case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : conflictingValues ? "All values rejected" : "Left unset";
534
561
  case "could-not-confirm": return "Could not confirm";
@@ -584,7 +611,16 @@ function isEmptyValue(value) {
584
611
  }
585
612
  const ARROW_SVG = "<svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><path d=\"M5 12h14M13 6l6 6-6 6\"/></svg>";
586
613
  const WARNING_SVG = "<svg width=\"15\" height=\"15\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" aria-hidden=\"true\"><path d=\"M12 9v4M12 17h.01\"/><path d=\"M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z\"/></svg>";
587
- function renderReviewQueueSessionHtml(session, presentationAdapter, queueView) {
614
+ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView, attestation, fieldStates) {
615
+ // A queue that does not match its stored extraction import is not reviewed:
616
+ // its items could hide or alter what the source said.
617
+ if (attestation.state === "diverges") {
618
+ return `
619
+ <section class="workbench-shell review" data-testid="review-workbench-shell" data-queue-attestation="diverges" aria-label="Survey review workbench">
620
+ <div class="queue-attestation diverges" data-testid="queue-attestation" data-state="diverges" role="alert">${WARNING_SVG}<span>${escapeHtml(attestation.message)}</span></div>
621
+ </section>
622
+ `;
623
+ }
588
624
  const totalCount = session.items.length;
589
625
  const decidedCount = session.items.filter((item) => session.decisionsByItemName[item.metadata.name] !== undefined
590
626
  || item.spec.candidateSetStatus === "resolved").length;
@@ -594,7 +630,13 @@ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView) {
594
630
  const headerTitle = typeof producerName === "string" && producerName.length > 0 ? producerName : "Review queue";
595
631
  const window = buildReviewQueueWindow(session, queueView);
596
632
  return `
597
- <section class="workbench-shell review" data-testid="review-workbench-shell" aria-label="Survey review workbench">
633
+ <section class="workbench-shell review" data-testid="review-workbench-shell" data-queue-attestation="${attestation.state}" aria-label="Survey review workbench">
634
+ ${attestation.state === "unverified"
635
+ ? `<div class="queue-attestation unverified" data-testid="queue-attestation" data-state="unverified" role="note">${WARNING_SVG}<span>${escapeHtml(attestation.message)}</span></div>`
636
+ : ""}
637
+ ${isScoreBlind(session)
638
+ ? `<div class="queue-attestation score-blind" data-testid="score-blind-notice" data-sampling="${escapeHtml(session.sampling?.kind ?? "")}" role="note"><span>${escapeHtml(scoreBlindNotice(session))}</span></div>`
639
+ : ""}
598
640
  <header class="rhead">
599
641
  <div class="top">
600
642
  <div class="subj">
@@ -611,6 +653,7 @@ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView) {
611
653
  <button class="apply" type="button" data-testid="apply-button"${decidedCount === 0 ? " disabled" : ""}>Apply ${decidedCount} decision${decidedCount === 1 ? "" : "s"}</button>
612
654
  </div>
613
655
  </header>
656
+ ${fieldStates ? renderFieldStatesHtml(fieldStates, session) : ""}
614
657
  <nav class="queue-controls" aria-label="Review queue navigation">
615
658
  <label>
616
659
  <span>Find fields</span>
@@ -654,9 +697,9 @@ function renderFieldCard(item, session, presentationAdapter) {
654
697
  const decided = decision !== undefined;
655
698
  const current = item.spec.candidates.find((candidate) => candidate.role === "current");
656
699
  const proposedCandidates = item.spec.candidates.filter((candidate) => candidate.role === "proposed");
657
- // Several proposed values for one claim are a conflict. A control names a
658
- // role, not a value, so none may make one of them the trusted value; the
659
- // reviewer can still reject them all or end the round as could-not-confirm.
700
+ // Several proposed values for one claim are a conflict. The reviewer can
701
+ // choose one of them by candidate id (select-proposed), reject them all, or
702
+ // end the round as could-not-confirm; no role-based control may pick one.
660
703
  const conflict = proposedCandidates.length > 1;
661
704
  const proposed = proposedCandidates.length === 1 ? proposedCandidates[0] : undefined;
662
705
  const presentation = buildReviewItemPresentation(item, presentationAdapter);
@@ -698,11 +741,11 @@ function renderFieldCard(item, session, presentationAdapter) {
698
741
  ${proposed
699
742
  ? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
700
743
  : proposedCandidates.length > 1
701
- ? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel)
744
+ ? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel, decision, session.selectedCandidateIdsByItemName?.[item.metadata.name])
702
745
  : "<p class=\"field-value\">No proposed value is available for this field.</p>"}
703
- ${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
746
+ ${proposed ? renderProvenanceRow(item, proposed, presentationAdapter, isScoreBlind(session)) : ""}
704
747
  ${renderExtractionImportNotes(presentation)}
705
- ${renderVerificationNotes(candidateVerificationNotes(item, decision === "accept-proposed" ? editedValue : undefined))}
748
+ ${isScoreBlind(session) ? "" : renderVerificationNotes(candidateVerificationNotes(item, decision === "accept-proposed" ? editedValue : undefined))}
706
749
  <div class="decide">
707
750
  ${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
708
751
  ${proposed ? `<button class="btn use" type="button" data-testid="use-proposed" data-item-name="${escapeHtml(item.metadata.name)}">Use proposed</button>
@@ -732,18 +775,37 @@ function renderFieldCard(item, session, presentationAdapter) {
732
775
  }
733
776
  /**
734
777
  * The card body for an item whose candidate set holds several proposed values.
735
- * Lists every value with its excerpt; the workbench records a decision against
736
- * a role, so it cannot pick one of them, and says what the reviewer can do.
778
+ * Lists every value with its excerpt. Until the field is decided, each value
779
+ * has its own "Use this value" control, which records `select-proposed` with
780
+ * that candidate's id. Once a value is chosen, every value stays listed and is
781
+ * marked chosen or not chosen, so the card never reads as if there had been
782
+ * one value. Proposals excluded at import are not candidates: they are listed
783
+ * by {@link renderExtractionImportNotes} and cannot be chosen.
737
784
  */
738
- function renderConflictingProposals(item, candidates, presentationAdapter, targetLabel) {
785
+ function renderConflictingProposals(item, candidates, presentationAdapter, targetLabel, decision, selectedCandidateId) {
786
+ const chose = decision === "select-proposed";
739
787
  const values = candidates.map((candidate) => {
740
788
  const text = buildReviewCandidatePresentation(item, candidate, presentationAdapter, targetLabel).valueText;
741
789
  const excerpt = candidate.locator?.excerpt;
742
- return `<li data-testid="conflicting-value"><span class="vtext">${escapeHtml(text)}</span>${excerpt ? ` <q>${escapeHtml(excerpt)}</q>` : ""}</li>`;
790
+ const chosen = chose && candidate.id === selectedCandidateId;
791
+ const mark = chose
792
+ ? ` <span class="choice-tag ${chosen ? "chosen" : "not-chosen"}" data-testid="choice-tag">${chosen ? "Chosen" : "Not chosen"}</span>`
793
+ : "";
794
+ const control = decision === undefined
795
+ ? `<button class="btn choose" type="button" data-testid="select-value" data-item-name="${escapeHtml(item.metadata.name)}" data-candidate-id="${escapeHtml(candidate.id)}" aria-label="Use ${escapeHtml(text)} for ${escapeHtml(targetLabel)}">Use this value</button>`
796
+ : "";
797
+ return `<li data-testid="conflicting-value" data-candidate-id="${escapeHtml(candidate.id)}"${chose ? ` data-chosen="${chosen ? "true" : "false"}"` : ""}>
798
+ <span class="conflict-text"><span class="vtext">${escapeHtml(text)}</span>${excerpt ? ` <q>${escapeHtml(excerpt)}</q>` : ""}${mark}</span>${control}
799
+ </li>`;
743
800
  }).join("");
801
+ const intro = decision === undefined
802
+ ? `${candidates.length} different values were proposed for this field. Use the value the source supports, reject them all, or mark the field Could not confirm with a reason.`
803
+ : chose
804
+ ? `${candidates.length} different values were proposed for this field. One was chosen; ${candidates.length === 2 ? "the other was" : "the others were"} seen and not chosen.`
805
+ : `${candidates.length} different values were proposed for this field.`;
744
806
  return `
745
- <div class="field-value" data-testid="conflicting-proposals">
746
- <p>${candidates.length} different values were proposed for this field. This queue cannot choose one of them yet: reject them all, or mark the field Could not confirm with a reason.</p>
807
+ <div class="field-value conflict-values" data-testid="conflicting-proposals">
808
+ <p>${escapeHtml(intro)}</p>
747
809
  <ul>${values}</ul>
748
810
  </div>
749
811
  `;
@@ -906,7 +968,7 @@ export function validateProposedValue(descriptor, rawValue) {
906
968
  return undefined;
907
969
  }
908
970
  }
909
- function renderProvenanceRow(item, proposed, presentationAdapter) {
971
+ function renderProvenanceRow(item, proposed, presentationAdapter, scoreBlind) {
910
972
  const excerpt = proposed.locator?.excerpt;
911
973
  if (!excerpt) {
912
974
  return `
@@ -919,7 +981,8 @@ function renderProvenanceRow(item, proposed, presentationAdapter) {
919
981
  </div>
920
982
  `;
921
983
  }
922
- const confidence = proposed.extraction.confidence ?? proposed.confidence;
984
+ // A score-blind session never renders the proposer's confidence.
985
+ const confidence = scoreBlind ? undefined : proposed.extraction.confidence ?? proposed.confidence;
923
986
  const presentation = buildReviewCandidatePresentation(item, proposed, presentationAdapter);
924
987
  const sourceLinkHtml = presentation.sourceLink
925
988
  ? `<a href="${escapeHtml(presentation.sourceLink.href)}">${escapeHtml(presentation.sourceLink.label ?? presentation.sourceText)}</a>`
@@ -944,7 +1007,8 @@ function renderProvenanceRow(item, proposed, presentationAdapter) {
944
1007
  */
945
1008
  function renderExtractionImportNotes(presentation) {
946
1009
  const { excerptVerification: verification, excludedProposals: excluded } = presentation;
947
- if (verification === undefined && excluded.length === 0)
1010
+ const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
1011
+ if (verification === undefined && excluded.length === 0 && unreadable === undefined)
948
1012
  return "";
949
1013
  const excludedHtml = excluded.length === 0 ? "" : `
950
1014
  <div class="noprov" data-testid="excluded-proposals" role="note">
@@ -954,12 +1018,18 @@ function renderExtractionImportNotes(presentation) {
954
1018
  ${excluded.map((entry) => `<span data-testid="excluded-proposal" data-proposal-index="${escapeHtml(String(entry.proposalIndex))}"><q>${escapeHtml(entry.valueText)}</q> (proposal ${escapeHtml(String(entry.proposalIndex))}, ${escapeHtml(entry.locator)})</span>`).join(", ")}.
955
1019
  Unverifiable is not disproven: check the source before deciding.</span>
956
1020
  </div>`;
1021
+ const unreadableHtml = unreadable === undefined ? "" : `
1022
+ <div class="noprov" data-testid="excluded-proposals-unreadable" data-reason="${escapeHtml(presentation.excludedProposalsUnreadable.reason)}" role="note">
1023
+ ${WARNING_SVG}
1024
+ <span class="tag">${presentation.excludedProposalsUnreadable.reason === "binding-broken" ? "Binding broken" : "Unreadable"}</span>
1025
+ <span>${escapeHtml(unreadable)} Check the source before deciding.</span>
1026
+ </div>`;
957
1027
  const verificationHtml = verification === undefined ? "" : `
958
1028
  <p class="excerpt-verification" data-testid="excerpt-verification" data-verified="${verification === "verified" ? "true" : "false"}">${verification === "verified"
959
1029
  ? "Excerpts checked against the prepared source text at import."
960
1030
  : "Excerpts not checked against the prepared source text at import."}</p>`;
961
1031
  return `
962
- <div class="prov import-notes">${excludedHtml}${verificationHtml}
1032
+ <div class="prov import-notes">${excludedHtml}${unreadableHtml}${verificationHtml}
963
1033
  </div>
964
1034
  `;
965
1035
  }
@@ -1005,10 +1075,13 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
1005
1075
  note,
1006
1076
  decision,
1007
1077
  editedValue: session.editedValuesByItemName?.[item.metadata.name],
1078
+ selectedCandidateId: session.selectedCandidateIdsByItemName?.[item.metadata.name],
1008
1079
  reviewedAt: session.reviewedAt,
1009
1080
  actorId: session.actorId,
1081
+ ...(session.presentation !== undefined ? { presentation: session.presentation } : {}),
1082
+ ...(session.sampling !== undefined ? { sampling: session.sampling } : {}),
1010
1083
  };
1011
- const reviewDecisionPayload = buildReviewDecision(state);
1084
+ const reviewDecisionPayload = buildReviewDecision(state, { presentationAdapter });
1012
1085
  const preview = buildSurfaceProjectionPreview(item, reviewDecisionPayload, presentationAdapter);
1013
1086
  // Seeded with what the card face already shows, so the audit surface never
1014
1087
  // reprints it. Passed down through every section: the sections render in DOM
@@ -1033,7 +1106,12 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
1033
1106
  ${proposed?.extraction.model ? fieldItem("model", "Model", proposed.extraction.model) : ""}
1034
1107
  ${proposed ? placementItem(trace, { of: proposed.id, property: "extraction.extractor" }, "extractor", "Extractor", proposed.extraction.extractor ?? "unknown") : ""}
1035
1108
  ${proposed ? fieldItem("extracted-at", "Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
1036
- ${buildReviewItemPresentation(item, presentationAdapter).excludedProposals.map((entry) => fieldItem("excluded-proposal", `Excluded proposal ${entry.proposalIndex}`, `${entry.valueText} at ${entry.locator}: excerpt not at the cited span`)).join("")}
1109
+ ${(() => {
1110
+ const itemPresentation = buildReviewItemPresentation(item, presentationAdapter);
1111
+ const unreadable = excludedProposalsUnreadableSentence(itemPresentation.excludedProposalsUnreadable);
1112
+ return itemPresentation.excludedProposals.map((entry) => fieldItem("excluded-proposal", `Excluded proposal ${entry.proposalIndex}`, `${entry.valueText} at ${entry.locator}: excerpt not at the cited span`)).join("")
1113
+ + (unreadable ? fieldItem("excluded-proposals-unreadable", "Excluded proposals not shown", unreadable) : "");
1114
+ })()}
1037
1115
  </dl>
1038
1116
  ${preview
1039
1117
  ? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview, trace)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
@@ -1257,6 +1335,9 @@ function createReviewWorkbenchController(root, startState, options) {
1257
1335
  let events = eventStore?.load(baseSession) ?? [];
1258
1336
  let session = events.length > 0 ? replayReviewSessionEvents(baseSession, events) : baseSession;
1259
1337
  let queueView = normalizeReviewQueueView({ pageSize: options.pageSize });
1338
+ // The queue's items never change after mount (decisions live in events), so
1339
+ // the check against its extraction import runs once.
1340
+ const attestation = attestReviewQueueExtraction(baseSession.items, options.extractionImport);
1260
1341
  const persistEvents = () => {
1261
1342
  eventStore?.save(session, events);
1262
1343
  };
@@ -1272,7 +1353,7 @@ function createReviewWorkbenchController(root, startState, options) {
1272
1353
  const session = sessionForEvent;
1273
1354
  const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
1274
1355
  const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
1275
- const candidateId = item && decision ? decisionCandidateId(item, decision) : undefined;
1356
+ const candidateId = item && decision ? decisionCandidateId(item, decision, session.selectedCandidateIdsByItemName?.[item.metadata.name]) : undefined;
1276
1357
  const definition = decision ? workbenchDecisionDefinitions[decision] : undefined;
1277
1358
  const note = itemName ? session.notesByItemName[itemName] : undefined;
1278
1359
  // Carry the reviewer's inline edit in the event (accept-proposed only), so
@@ -1283,10 +1364,14 @@ function createReviewWorkbenchController(root, startState, options) {
1283
1364
  const attemptEvidenceIds = itemName && decision === "could-not-confirm"
1284
1365
  ? session.attemptEvidenceIdsByItemName?.[itemName]
1285
1366
  : undefined;
1367
+ const conditions = sessionConditionsStamp(session);
1286
1368
  const defaultData = decision
1287
- ? (editedValue !== undefined
1288
- ? { workbenchDecision: decision, workbenchEditedValue: editedValue }
1289
- : { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) })
1369
+ ? {
1370
+ ...(editedValue !== undefined
1371
+ ? { workbenchDecision: decision, workbenchEditedValue: editedValue }
1372
+ : { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) }),
1373
+ ...(conditions ? { sessionConditions: conditions } : {}),
1374
+ }
1290
1375
  : undefined;
1291
1376
  return buildReviewSessionEvent(session, {
1292
1377
  sessionName: defaultReviewSessionName,
@@ -1315,7 +1400,7 @@ function createReviewWorkbenchController(root, startState, options) {
1315
1400
  events = [...events, event];
1316
1401
  persistEvents();
1317
1402
  };
1318
- const setDecision = (itemName, decision, rawEditedValue) => {
1403
+ const setDecision = (itemName, decision, rawEditedValue, selectedCandidateId) => {
1319
1404
  if (decision === "could-not-confirm" && !session.notesByItemName[itemName]?.trim()) {
1320
1405
  throw new Error("Could not confirm requires a non-empty reason.");
1321
1406
  }
@@ -1334,11 +1419,19 @@ function createReviewWorkbenchController(root, startState, options) {
1334
1419
  else {
1335
1420
  delete nextEditedValuesByItemName[itemName];
1336
1421
  }
1422
+ const nextSelections = { ...session.selectedCandidateIdsByItemName };
1423
+ if (decision === "select-proposed" && selectedCandidateId !== undefined) {
1424
+ nextSelections[itemName] = selectedCandidateId;
1425
+ }
1426
+ else {
1427
+ delete nextSelections[itemName];
1428
+ }
1337
1429
  commitSessionUpdate({
1338
1430
  ...session,
1339
1431
  activeItemName: itemName,
1340
1432
  decisionsByItemName: { ...session.decisionsByItemName, [itemName]: decision },
1341
1433
  editedValuesByItemName: nextEditedValuesByItemName,
1434
+ selectedCandidateIdsByItemName: nextSelections,
1342
1435
  }, "decision-changed", itemName);
1343
1436
  };
1344
1437
  const clearDecision = (itemName) => {
@@ -1348,12 +1441,15 @@ function createReviewWorkbenchController(root, startState, options) {
1348
1441
  delete remainingEdits[itemName];
1349
1442
  const remainingAttemptEvidenceIds = { ...session.attemptEvidenceIdsByItemName };
1350
1443
  delete remainingAttemptEvidenceIds[itemName];
1444
+ const remainingSelections = { ...session.selectedCandidateIdsByItemName };
1445
+ delete remainingSelections[itemName];
1351
1446
  commitSessionUpdate({
1352
1447
  ...session,
1353
1448
  activeItemName: itemName,
1354
1449
  decisionsByItemName: remainingDecisions,
1355
1450
  editedValuesByItemName: remainingEdits,
1356
1451
  attemptEvidenceIdsByItemName: remainingAttemptEvidenceIds,
1452
+ selectedCandidateIdsByItemName: remainingSelections,
1357
1453
  }, "decision-changed", itemName, { workbenchDecision: null });
1358
1454
  };
1359
1455
  const updateReviewerNote = (itemName, note) => {
@@ -1361,8 +1457,9 @@ function createReviewWorkbenchController(root, startState, options) {
1361
1457
  };
1362
1458
  const controller = {
1363
1459
  currentSession: () => session,
1364
- currentSessionExport: () => buildReviewWorkbenchSessionExport(session, events),
1365
- renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter, queueView),
1460
+ currentSessionExport: () => buildReviewWorkbenchSessionExport(session, events, { presentationAdapter: options.presentationAdapter }),
1461
+ presentationAdapter: options.presentationAdapter,
1462
+ renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter, queueView, attestation, options.fieldStates),
1366
1463
  setDecision,
1367
1464
  clearDecision,
1368
1465
  updateReviewerNote,
@@ -1382,8 +1479,8 @@ function reviewSessionStorageKey(session, keyPrefix) {
1382
1479
  const itemNames = buildReviewSessionResource(session).spec.reviewItemNames.join(",");
1383
1480
  return `${keyPrefix}:${itemNames}`;
1384
1481
  }
1385
- function renderCurrentState(root, session, controller, presentationAdapter, queueView) {
1386
- root.innerHTML = renderReviewWorkbenchHtml(session, undefined, { presentationAdapter, queueView });
1482
+ function renderCurrentState(root, session, controller, presentationAdapter, queueView, attestation, fieldStates) {
1483
+ root.innerHTML = renderReviewQueueSessionHtml(session, presentationAdapter, queueView ?? normalizeReviewQueueView(undefined), attestation ?? attestReviewQueueExtraction(session.items, undefined), fieldStates);
1387
1484
  bindFieldCardInteractions(root, controller);
1388
1485
  bindApplyButton(root, controller);
1389
1486
  bindClampToggles(root);
@@ -1431,10 +1528,19 @@ function queueSessionFromStartState(startState) {
1431
1528
  ...(startState.attemptEvidenceIds?.length
1432
1529
  ? { attemptEvidenceIdsByItemName: { [startState.item.metadata.name]: [...startState.attemptEvidenceIds] } }
1433
1530
  : {}),
1531
+ ...(startState.decision === "select-proposed" && startState.selectedCandidateId !== undefined
1532
+ ? { selectedCandidateIdsByItemName: { [startState.item.metadata.name]: startState.selectedCandidateId } }
1533
+ : {}),
1434
1534
  reviewedAt: startState.reviewedAt,
1435
1535
  actorId: startState.actorId,
1536
+ ...(startState.presentation !== undefined ? { presentation: startState.presentation } : {}),
1537
+ ...(startState.sampling !== undefined ? { sampling: startState.sampling } : {}),
1436
1538
  };
1437
1539
  }
1540
+ function scoreBlindNotice(session) {
1541
+ const audit = session.sampling?.kind === "random-audit" ? "Random audit sample. " : "";
1542
+ return `${audit}Score-blind review: confidence and verifier results are hidden for this whole session. Decide from the source and its excerpt.`;
1543
+ }
1438
1544
  const COULD_NOT_CONFIRM_REASON_REQUIRED = "A reason is required when you could not confirm.";
1439
1545
  /**
1440
1546
  * Opens every collapsed region between `control` and the card before anything
@@ -1488,6 +1594,21 @@ function bindFieldCardInteractions(root, controller) {
1488
1594
  controller.renderCurrentState();
1489
1595
  });
1490
1596
  });
1597
+ // One value of a conflict: the button names its candidate, and the decision
1598
+ // records that id. The builder refuses an id that is not one of the item's
1599
+ // proposed candidates, so a stale or forged button changes nothing.
1600
+ root.querySelectorAll("[data-testid='select-value']").forEach((button) => {
1601
+ button.addEventListener("click", () => {
1602
+ const itemName = button.dataset.itemName ?? "";
1603
+ const candidateId = button.dataset.candidateId ?? "";
1604
+ const item = controller.currentSession().items.find((entry) => entry.metadata.name === itemName);
1605
+ if (!item || conflictSelectionIssue(item, candidateId)) {
1606
+ return;
1607
+ }
1608
+ controller.setDecision(itemName, "select-proposed", undefined, candidateId);
1609
+ controller.renderCurrentState();
1610
+ });
1611
+ });
1491
1612
  root.querySelectorAll("[data-testid='keep-current']").forEach((button) => {
1492
1613
  button.addEventListener("click", () => {
1493
1614
  const itemName = button.dataset.itemName ?? "";
@@ -1598,7 +1719,7 @@ function refreshAuditPayloadForItem(textarea, controller, itemName) {
1598
1719
  reviewedAt: session.reviewedAt,
1599
1720
  actorId: session.actorId,
1600
1721
  };
1601
- payload.textContent = JSON.stringify(buildReviewDecision(state) ?? null, null, 2);
1722
+ payload.textContent = JSON.stringify(buildReviewDecision(state, { presentationAdapter: controller.presentationAdapter }) ?? null, null, 2);
1602
1723
  }
1603
1724
  function bindClampToggles(root) {
1604
1725
  root.querySelectorAll("[data-clamp-toggle]").forEach((button) => {