@kontourai/survey 7.0.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 (31) hide show
  1. package/dist/example-data/public-directory-review-resource.d.ts +3 -3
  2. package/dist/src/calibration.d.ts +15 -0
  3. package/dist/src/calibration.js +11 -0
  4. package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
  5. package/dist/src/canonical-reviewed-trust-input.js +15 -0
  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 +3 -1
  13. package/dist/src/index.js +2 -0
  14. package/dist/src/mcp/review-mcp.js +35 -10
  15. package/dist/src/review-resource.d.ts +29 -0
  16. package/dist/src/review-workbench/audit-sample.d.ts +32 -0
  17. package/dist/src/review-workbench/audit-sample.js +53 -0
  18. package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
  19. package/dist/src/review-workbench/extraction-inspector.js +1 -1
  20. package/dist/src/review-workbench/field-state-view.d.ts +15 -0
  21. package/dist/src/review-workbench/field-state-view.js +84 -0
  22. package/dist/src/review-workbench/review-queue-session.d.ts +46 -1
  23. package/dist/src/review-workbench/review-queue-session.js +74 -3
  24. package/dist/src/review-workbench/review-session-replay.d.ts +1 -1
  25. package/dist/src/review-workbench/review-session-replay.js +15 -1
  26. package/dist/src/review-workbench/review-workbench-css.generated.js +32 -0
  27. package/dist/src/review-workbench/review-workbench.css +32 -0
  28. package/dist/src/review-workbench/review-workbench.d.ts +13 -1
  29. package/dist/src/review-workbench/review-workbench.js +41 -15
  30. package/dist/src/review-workbench/review-workbench.standalone.css +32 -0
  31. package/package.json +1 -1
@@ -0,0 +1,84 @@
1
+ import { isScoreBlind } from "./review-queue-session.js";
2
+ const CONTENT_TEXT = {
3
+ value: "Value found",
4
+ conflicting: "Conflicting values",
5
+ unsupported: "Not supported by verifier",
6
+ excluded: "Proposals excluded",
7
+ not_covered: "Not read",
8
+ };
9
+ const LIFECYCLE_TEXT = {
10
+ pending: "Needs review",
11
+ accepted: "Accepted",
12
+ rejected: "Rejected",
13
+ could_not_confirm: "Could not confirm",
14
+ superseded: "Superseded",
15
+ };
16
+ const WORKBENCH_LIFECYCLE = {
17
+ "accept-proposed": "accepted",
18
+ "select-proposed": "accepted",
19
+ "keep-current": "accepted",
20
+ "reject-proposed": "rejected",
21
+ "could-not-confirm": "could_not_confirm",
22
+ };
23
+ /**
24
+ * The field-state panel: one row per field the extraction was asked for or
25
+ * proposed, with its derived content state, its lifecycle, and its signals.
26
+ * Fields with no review card (not read, only excluded proposals, no proposal)
27
+ * are listed too, so a run that lost content never reads as complete.
28
+ *
29
+ * The lifecycle of a field in the queue is read from the live session, so it
30
+ * follows the reviewer's decisions. Only what the session cannot know comes
31
+ * from the derived states: a decision carried forward from an earlier round,
32
+ * and a superseded one. In a score-blind session a content state derived from
33
+ * verifier records is shown as the state its candidates alone give.
34
+ */
35
+ export function renderFieldStatesHtml(states, session) {
36
+ if (states.length === 0)
37
+ return "";
38
+ const itemsByName = new Map(session.items.map((item) => [item.metadata.name, item]));
39
+ const rows = states.map((state) => {
40
+ const item = state.reviewItemName !== undefined ? itemsByName.get(state.reviewItemName) : undefined;
41
+ const content = state.content === "unsupported" && isScoreBlind(session)
42
+ ? (item && item.spec.candidates.filter((candidate) => candidate.role === "proposed").length > 1 ? "conflicting" : "value")
43
+ : state.content;
44
+ const live = item ? session.decisionsByItemName[item.metadata.name] : undefined;
45
+ const lifecycle = live !== undefined
46
+ ? WORKBENCH_LIFECYCLE[live]
47
+ : state.lifecycle === "superseded" || state.decisionBasis === "carried-forward"
48
+ ? state.lifecycle
49
+ : state.reviewItemName !== undefined ? "pending" : undefined;
50
+ const carried = live === undefined && state.decisionBasis === "carried-forward";
51
+ const signals = [
52
+ ...(state.signals.incompleteRun ? ["The extraction stopped before reading all of the source."] : []),
53
+ ...(state.signals.excludedProposals ? [`${state.signals.excludedProposals} proposal${state.signals.excludedProposals === 1 ? " was" : "s were"} excluded: the excerpt is not at the cited span.`] : []),
54
+ ...(state.signals.unresolvedImport ? ["The import could not ground any value."] : []),
55
+ ];
56
+ const label = `${state.slot.fieldOrBehavior}${state.slot.pathIndices?.length ? `[${state.slot.pathIndices.join("][")}]` : ""}`;
57
+ return `
58
+ <li class="field-state-row" data-testid="field-state" data-field="${escapeHtml(label)}" data-content="${content ?? "none"}" data-lifecycle="${lifecycle ?? "none"}"${carried ? ' data-basis="carried-forward"' : ""}>
59
+ <span class="field-state-name">${escapeHtml(label)}</span>
60
+ <span class="field-state-chips">
61
+ <span class="chip state-content ${content ?? "none"}" data-testid="field-state-content">${escapeHtml(content === undefined ? "No proposal" : CONTENT_TEXT[content])}</span>
62
+ ${lifecycle === undefined ? "" : `<span class="chip state-lifecycle ${lifecycle}" data-testid="field-state-lifecycle">${escapeHtml(LIFECYCLE_TEXT[lifecycle])}${carried ? " · carried forward" : ""}</span>`}
63
+ </span>
64
+ ${content === undefined ? `<span class="field-state-note">No value was proposed. That is not the same as the source not stating one.</span>` : ""}
65
+ ${signals.map((signal) => `<span class="field-state-note signal" data-testid="field-state-signal">${escapeHtml(signal)}</span>`).join("")}
66
+ </li>`;
67
+ }).join("");
68
+ const unread = states.filter((state) => state.content === "not_covered").length;
69
+ return `
70
+ <section class="field-states" data-testid="field-states" aria-label="Field states">
71
+ <h2>Field states</h2>
72
+ ${unread > 0 ? `<p class="field-states-lead" data-testid="field-states-incomplete">${unread} field${unread === 1 ? " was" : "s were"} not read: the extraction stopped short. ${unread === 1 ? "It has" : "They have"} no value to review, and that does not mean the source lacks ${unread === 1 ? "it" : "them"}.</p>` : ""}
73
+ <ul>${rows}</ul>
74
+ </section>
75
+ `;
76
+ }
77
+ function escapeHtml(value) {
78
+ return String(value)
79
+ .replaceAll("&", "&amp;")
80
+ .replaceAll("<", "&lt;")
81
+ .replaceAll(">", "&gt;")
82
+ .replaceAll('"', "&quot;")
83
+ .replaceAll("'", "&#39;");
84
+ }
@@ -1,4 +1,5 @@
1
- import { type ReviewCandidate, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewSessionEventSpec } from "../../src/review-resource.js";
1
+ import { type ReviewCandidate, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewSessionEventSpec, type ReviewSessionPresentation, type ReviewSessionSampling } from "../../src/review-resource.js";
2
+ export type { ReviewSessionPresentation, ReviewSessionSampling } from "../../src/review-resource.js";
2
3
  /**
3
4
  * The decisions a reviewer can record on a ReviewItem.
4
5
  *
@@ -31,6 +32,10 @@ export interface ReviewWorkbenchState {
31
32
  * decision and ignored for every other one.
32
33
  */
33
34
  readonly selectedCandidateId?: string;
35
+ /** The session's presentation; recorded on the decision. See {@link ReviewSessionPresentation}. */
36
+ readonly presentation?: ReviewSessionPresentation;
37
+ /** How the session's items were chosen; recorded on the decision. */
38
+ readonly sampling?: ReviewSessionSampling;
34
39
  }
35
40
  export interface ReviewQueueSessionState {
36
41
  readonly items: readonly ReviewItem[];
@@ -53,7 +58,47 @@ export interface ReviewQueueSessionState {
53
58
  * `select-proposed` decision and never read it.
54
59
  */
55
60
  readonly selectedCandidateIdsByItemName?: Readonly<Record<string, string>>;
61
+ /**
62
+ * Session-level presentation, set when the session is opened. Every
63
+ * decision event is stamped with it and replay refuses a mismatch; a
64
+ * `ReviewQueueBinding` also covers it. Without a binding, an edit to both the
65
+ * snapshot and every event stamp is not detectable. Absent: scores are shown
66
+ * and decisions record no presentation.
67
+ */
68
+ readonly presentation?: ReviewSessionPresentation;
69
+ /** How the session's items were chosen. Absent: decisions record no sampling. */
70
+ readonly sampling?: ReviewSessionSampling;
56
71
  }
72
+ /** Whether the session hides confidence and verifier results from its reviewer. */
73
+ export declare function isScoreBlind(session: {
74
+ readonly presentation?: ReviewSessionPresentation;
75
+ }): boolean;
76
+ /**
77
+ * Throws unless the session's presentation and sampling are well formed:
78
+ * `scoreBlind` a boolean, and a `random-audit` sampling with a rate in (0, 1]
79
+ * and a non-empty seed. Unknown keys are refused so a decision never records
80
+ * a condition nothing reads.
81
+ */
82
+ export declare function assertReviewSessionConditions(session: {
83
+ readonly presentation?: unknown;
84
+ readonly sampling?: unknown;
85
+ }): void;
86
+ /**
87
+ * The session conditions a decision event carries, or `undefined` for a
88
+ * session that declares none. Stamped on every decision event so replay can
89
+ * refuse events recorded under other conditions than the snapshot's: sighted
90
+ * events replayed over a blind snapshot, or a snapshot whose presentation was
91
+ * changed after decisions were recorded.
92
+ */
93
+ export declare function sessionConditionsStamp(session: {
94
+ readonly presentation?: ReviewSessionPresentation;
95
+ readonly sampling?: ReviewSessionSampling;
96
+ }): {
97
+ presentation?: ReviewSessionPresentation;
98
+ sampling?: ReviewSessionSampling;
99
+ } | undefined;
100
+ export declare function assertAuditRate(rate: unknown): asserts rate is number;
101
+ export declare function assertAuditSeed(seed: unknown): asserts seed is string;
57
102
  export interface ReviewSessionSummary {
58
103
  readonly accepted: number;
59
104
  readonly keptCurrent: number;
@@ -4,6 +4,71 @@ import { checkEditedValueForItem } from "./edited-value.js";
4
4
  import { assertSoleCandidateId, reviewResourceApiVersion, } from "../../src/review-resource.js";
5
5
  export const reviewWorkbenchSessionStorageKey = "kontourai.survey.review-workbench.session-events.v1";
6
6
  export const defaultReviewSessionName = "review-workbench-session";
7
+ /** Whether the session hides confidence and verifier results from its reviewer. */
8
+ export function isScoreBlind(session) {
9
+ return session.presentation?.scoreBlind === true;
10
+ }
11
+ /**
12
+ * Throws unless the session's presentation and sampling are well formed:
13
+ * `scoreBlind` a boolean, and a `random-audit` sampling with a rate in (0, 1]
14
+ * and a non-empty seed. Unknown keys are refused so a decision never records
15
+ * a condition nothing reads.
16
+ */
17
+ export function assertReviewSessionConditions(session) {
18
+ const { presentation, sampling } = session;
19
+ if (presentation !== undefined) {
20
+ if (!isPlainRecord(presentation) || Object.keys(presentation).join(",") !== "scoreBlind" || typeof presentation.scoreBlind !== "boolean") {
21
+ throw new Error("Review session presentation must be { scoreBlind: boolean }.");
22
+ }
23
+ }
24
+ if (sampling !== undefined) {
25
+ if (!isPlainRecord(sampling))
26
+ throw new Error("Review session sampling must be an object.");
27
+ const keys = Object.keys(sampling).sort().join(",");
28
+ if (sampling.kind === "queue") {
29
+ if (keys !== "kind")
30
+ throw new Error("Review session sampling of kind queue takes no rate or seed.");
31
+ }
32
+ else if (sampling.kind === "random-audit") {
33
+ if (keys !== "kind,rate,seed")
34
+ throw new Error("Review session sampling of kind random-audit requires exactly rate and seed.");
35
+ assertAuditRate(sampling.rate);
36
+ assertAuditSeed(sampling.seed);
37
+ }
38
+ else {
39
+ throw new Error("Review session sampling kind must be queue or random-audit.");
40
+ }
41
+ }
42
+ }
43
+ /**
44
+ * The session conditions a decision event carries, or `undefined` for a
45
+ * session that declares none. Stamped on every decision event so replay can
46
+ * refuse events recorded under other conditions than the snapshot's: sighted
47
+ * events replayed over a blind snapshot, or a snapshot whose presentation was
48
+ * changed after decisions were recorded.
49
+ */
50
+ export function sessionConditionsStamp(session) {
51
+ if (session.presentation === undefined && session.sampling === undefined)
52
+ return undefined;
53
+ return {
54
+ ...(session.presentation !== undefined ? { presentation: { scoreBlind: session.presentation.scoreBlind } } : {}),
55
+ ...(session.sampling !== undefined ? { sampling: { ...session.sampling } } : {}),
56
+ };
57
+ }
58
+ export function assertAuditRate(rate) {
59
+ if (typeof rate !== "number" || !Number.isFinite(rate) || rate <= 0 || rate > 1) {
60
+ throw new Error("A random-audit rate must be a number greater than 0 and at most 1.");
61
+ }
62
+ }
63
+ export function assertAuditSeed(seed) {
64
+ if (typeof seed !== "string" || !AUDIT_SEED.test(seed)) {
65
+ throw new Error("A random-audit seed must be a stable identity: 1-256 characters from letters, digits and . _ : @ / + ~ -, starting with a letter or digit.");
66
+ }
67
+ }
68
+ const AUDIT_SEED = /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/;
69
+ function isPlainRecord(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
7
72
  export const workbenchDecisionDefinitions = {
8
73
  "accept-proposed": {
9
74
  label: "Accept proposed",
@@ -67,6 +132,8 @@ export function currentReviewWorkbenchState(session) {
67
132
  selectedCandidateId: session.selectedCandidateIdsByItemName?.[item.metadata.name],
68
133
  reviewedAt: session.reviewedAt,
69
134
  actorId: session.actorId,
135
+ ...(session.presentation !== undefined ? { presentation: session.presentation } : {}),
136
+ ...(session.sampling !== undefined ? { sampling: session.sampling } : {}),
70
137
  };
71
138
  }
72
139
  export function currentReviewItem(session) {
@@ -360,9 +427,13 @@ export function buildReviewSessionEvents(session, sessionName = defaultReviewSes
360
427
  actor: session.actorId,
361
428
  reviewedAt: session.reviewedAt,
362
429
  });
363
- const data = editedValue !== undefined
364
- ? { workbenchDecision: decision, workbenchEditedValue: editedValue }
365
- : { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) };
430
+ const conditions = sessionConditionsStamp(session);
431
+ const data = {
432
+ ...(editedValue !== undefined
433
+ ? { workbenchDecision: decision, workbenchEditedValue: editedValue }
434
+ : { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) }),
435
+ ...(conditions ? { sessionConditions: conditions } : {}),
436
+ };
366
437
  events.push(buildReviewSessionEvent(session, {
367
438
  sessionName,
368
439
  sequence: events.length + 1,
@@ -1,6 +1,6 @@
1
1
  import type { ReviewSessionEvent } from "../review-resource.js";
2
2
  import { type ReviewQueueSessionState } from "./review-queue-session.js";
3
- export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "invalid-conflict-selection" | "decision-status-mismatch" | "decision-resolution-mismatch" | "missing-resolution-reason" | "edited-value-not-editable" | "edited-value-type-mismatch";
3
+ export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "invalid-conflict-selection" | "decision-status-mismatch" | "decision-resolution-mismatch" | "missing-resolution-reason" | "edited-value-not-editable" | "edited-value-type-mismatch" | "session-conditions-mismatch";
4
4
  export interface ReviewSessionReplayIssue {
5
5
  readonly code: ReviewSessionReplayIssueCode;
6
6
  readonly eventName: string;
@@ -1,5 +1,6 @@
1
- import { candidateForDecision, conflictSelectionIssue, decisionSelectsNoCandidate, isClearedWorkbenchDecisionEvent, workbenchDecisionDefinitions, } from "./review-queue-session.js";
1
+ import { candidateForDecision, conflictSelectionIssue, decisionSelectsNoCandidate, isClearedWorkbenchDecisionEvent, sessionConditionsStamp, workbenchDecisionDefinitions, } from "./review-queue-session.js";
2
2
  import { checkEditedValueForItem } from "./edited-value.js";
3
+ import { canonicalJson } from "./canonical.js";
3
4
  export function validateReviewSessionEventsForSnapshot(snapshot, events) {
4
5
  const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
5
6
  const sequenceIssues = validateEventSequence(events);
@@ -44,6 +45,19 @@ export function validateReviewSessionEventsForSnapshot(snapshot, events) {
44
45
  }
45
46
  else if (event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted") {
46
47
  const decision = replayableWorkbenchDecision(event.spec.data?.workbenchDecision);
48
+ // A decision recorded under other presentation or sampling than the
49
+ // snapshot's must not be replayed as if made under the snapshot's.
50
+ const expectedConditions = sessionConditionsStamp(snapshot);
51
+ const carried = event.spec.data?.sessionConditions;
52
+ if (decision && canonicalJson(carried ?? null) !== canonicalJson(expectedConditions ?? null)) {
53
+ issues.push({
54
+ ...eventRef,
55
+ code: "session-conditions-mismatch",
56
+ reviewItemName,
57
+ candidateId: event.spec.candidateId,
58
+ message: `ReviewSessionEvent ${event.metadata.name} was recorded under session conditions ${JSON.stringify(carried ?? "none")}, but the snapshot's are ${JSON.stringify(expectedConditions ?? "none")}.`,
59
+ });
60
+ }
47
61
  if (!decision) {
48
62
  issues.push({
49
63
  ...eventRef,
@@ -617,6 +617,30 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
617
617
  .survey-workbench-embed .chip.rejected{ color: var(--k-negative); background: var(--k-negative-wash); }
618
618
  .survey-workbench-embed .chip.could-not-confirm{ color: var(--k-caution); background: var(--k-caution-wash); }
619
619
 
620
+ /* Field-state panel: one row per field, content and lifecycle derived from
621
+ the records, never stored labels. A field that was not read is listed. */
622
+ .survey-workbench-embed .field-states{ padding: var(--k-space-3, 12px) 1rem; border-bottom: 1px solid var(--k-line); background: var(--k-panel); }
623
+ .survey-workbench-embed .field-states h2{ margin: 0 0 8px; font-size: 13px; font-weight: 700; color: var(--k-text); }
624
+ .survey-workbench-embed .field-states ul{ list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
625
+ .survey-workbench-embed .field-states-lead{ margin: 0 0 8px; font-size: 13px; line-height: 1.45; color: var(--k-caution); }
626
+ .survey-workbench-embed .field-state-row{ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; align-items: center; padding: 6px 0; border-top: 1px solid var(--k-line); }
627
+ .survey-workbench-embed .field-state-row:first-child{ border-top: 0; }
628
+ .survey-workbench-embed .field-state-name{ font-family: var(--k-font-mono, ui-monospace, monospace); font-size: 12px; color: var(--k-text); overflow-wrap: anywhere; }
629
+ .survey-workbench-embed .field-state-chips{ display: inline-flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
630
+ .survey-workbench-embed .field-state-note{ grid-column: 1 / -1; font-size: 12px; color: var(--k-muted); }
631
+ .survey-workbench-embed .field-state-note.signal{ color: var(--k-caution); }
632
+ .survey-workbench-embed .chip.state-content.value{ color: var(--k-positive); background: var(--k-positive-wash); }
633
+ .survey-workbench-embed .chip.state-content.conflicting,
634
+ .survey-workbench-embed .chip.state-content.excluded,
635
+ .survey-workbench-embed .chip.state-content.not_covered{ color: var(--k-caution); background: var(--k-caution-wash); }
636
+ .survey-workbench-embed .chip.state-content.unsupported{ color: var(--k-negative); background: var(--k-negative-wash); }
637
+ .survey-workbench-embed .chip.state-content.none,
638
+ .survey-workbench-embed .chip.state-lifecycle.superseded{ color: var(--k-muted); background: var(--k-sunken); }
639
+ .survey-workbench-embed .chip.state-lifecycle.pending{ color: var(--k-brand); background: var(--k-brand-wash); }
640
+ .survey-workbench-embed .chip.state-lifecycle.accepted{ color: var(--k-positive); background: var(--k-positive-wash); }
641
+ .survey-workbench-embed .chip.state-lifecycle.rejected{ color: var(--k-negative); background: var(--k-negative-wash); }
642
+ .survey-workbench-embed .chip.state-lifecycle.could_not_confirm{ color: var(--k-caution); background: var(--k-caution-wash); }
643
+
620
644
  .survey-workbench-embed .frow1 .push{
621
645
  margin-left: auto;
622
646
  }
@@ -861,6 +885,12 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
861
885
  background: var(--k-negative-wash);
862
886
  }
863
887
 
888
+ /* A score-blind session: an informational note, not a warning. */
889
+ .survey-workbench-embed .queue-attestation.score-blind{
890
+ color: var(--k-active);
891
+ background: var(--k-active-soft);
892
+ }
893
+
864
894
  /* Conflicting proposed values: each value is listed with its own choice
865
895
  control until the field is decided, then marked chosen or not chosen. */
866
896
  .survey-workbench-embed .conflict-values ul{
@@ -1421,6 +1451,8 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
1421
1451
  /* The diff-collapse must also hold in host contexts without a query container
1422
1452
  (e.g. an embed that doesn't establish \`container-type\`): fall back to viewport. */
1423
1453
  @media (max-width: 620px) {
1454
+ .survey-workbench-embed .field-state-row{ grid-template-columns: minmax(0, 1fr); }
1455
+ .survey-workbench-embed .field-state-chips{ justify-content: flex-start; }
1424
1456
  .survey-workbench-embed .diff{
1425
1457
  grid-template-columns: 1fr;
1426
1458
  }
@@ -614,6 +614,30 @@
614
614
  .survey-workbench-embed .chip.rejected{ color: var(--k-negative); background: var(--k-negative-wash); }
615
615
  .survey-workbench-embed .chip.could-not-confirm{ color: var(--k-caution); background: var(--k-caution-wash); }
616
616
 
617
+ /* Field-state panel: one row per field, content and lifecycle derived from
618
+ the records, never stored labels. A field that was not read is listed. */
619
+ .survey-workbench-embed .field-states{ padding: var(--k-space-3, 12px) 1rem; border-bottom: 1px solid var(--k-line); background: var(--k-panel); }
620
+ .survey-workbench-embed .field-states h2{ margin: 0 0 8px; font-size: 13px; font-weight: 700; color: var(--k-text); }
621
+ .survey-workbench-embed .field-states ul{ list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
622
+ .survey-workbench-embed .field-states-lead{ margin: 0 0 8px; font-size: 13px; line-height: 1.45; color: var(--k-caution); }
623
+ .survey-workbench-embed .field-state-row{ display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 4px 12px; align-items: center; padding: 6px 0; border-top: 1px solid var(--k-line); }
624
+ .survey-workbench-embed .field-state-row:first-child{ border-top: 0; }
625
+ .survey-workbench-embed .field-state-name{ font-family: var(--k-font-mono, ui-monospace, monospace); font-size: 12px; color: var(--k-text); overflow-wrap: anywhere; }
626
+ .survey-workbench-embed .field-state-chips{ display: inline-flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
627
+ .survey-workbench-embed .field-state-note{ grid-column: 1 / -1; font-size: 12px; color: var(--k-muted); }
628
+ .survey-workbench-embed .field-state-note.signal{ color: var(--k-caution); }
629
+ .survey-workbench-embed .chip.state-content.value{ color: var(--k-positive); background: var(--k-positive-wash); }
630
+ .survey-workbench-embed .chip.state-content.conflicting,
631
+ .survey-workbench-embed .chip.state-content.excluded,
632
+ .survey-workbench-embed .chip.state-content.not_covered{ color: var(--k-caution); background: var(--k-caution-wash); }
633
+ .survey-workbench-embed .chip.state-content.unsupported{ color: var(--k-negative); background: var(--k-negative-wash); }
634
+ .survey-workbench-embed .chip.state-content.none,
635
+ .survey-workbench-embed .chip.state-lifecycle.superseded{ color: var(--k-muted); background: var(--k-sunken); }
636
+ .survey-workbench-embed .chip.state-lifecycle.pending{ color: var(--k-brand); background: var(--k-brand-wash); }
637
+ .survey-workbench-embed .chip.state-lifecycle.accepted{ color: var(--k-positive); background: var(--k-positive-wash); }
638
+ .survey-workbench-embed .chip.state-lifecycle.rejected{ color: var(--k-negative); background: var(--k-negative-wash); }
639
+ .survey-workbench-embed .chip.state-lifecycle.could_not_confirm{ color: var(--k-caution); background: var(--k-caution-wash); }
640
+
617
641
  .survey-workbench-embed .frow1 .push{
618
642
  margin-left: auto;
619
643
  }
@@ -858,6 +882,12 @@
858
882
  background: var(--k-negative-wash);
859
883
  }
860
884
 
885
+ /* A score-blind session: an informational note, not a warning. */
886
+ .survey-workbench-embed .queue-attestation.score-blind{
887
+ color: var(--k-active);
888
+ background: var(--k-active-soft);
889
+ }
890
+
861
891
  /* Conflicting proposed values: each value is listed with its own choice
862
892
  control until the field is decided, then marked chosen or not chosen. */
863
893
  .survey-workbench-embed .conflict-values ul{
@@ -1418,6 +1448,8 @@
1418
1448
  /* The diff-collapse must also hold in host contexts without a query container
1419
1449
  (e.g. an embed that doesn't establish `container-type`): fall back to viewport. */
1420
1450
  @media (max-width: 620px) {
1451
+ .survey-workbench-embed .field-state-row{ grid-template-columns: minmax(0, 1fr); }
1452
+ .survey-workbench-embed .field-state-chips{ justify-content: flex-start; }
1421
1453
  .survey-workbench-embed .diff{
1422
1454
  grid-template-columns: 1fr;
1423
1455
  }
@@ -3,10 +3,13 @@ import { type ReviewSessionReplayIssue, type ReviewSessionReplayWarning } from "
3
3
  import { type ReviewPresentationAdapter } from "./review-presentation.js";
4
4
  import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewValueDescriptor } from "../review-resource.js";
5
5
  import type { ExtractionEnvelopeImport, ExtractionEnvelopeImportResult } from "../extraction-envelope.js";
6
+ import type { FieldState } from "../field-states.js";
6
7
  export { reviewAuditRowKeys, type ReviewAuditRowKey } from "./audit-rows.js";
7
8
  export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, attestReviewQueueExtraction, bindReviewQueue, reviewItemCarriesExtractionBinding, unverifiedExtractionQueueSentence, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, type BindReviewQueueOptions, type ReviewQueueBinding, type ReviewQueueBindingIssue, type ReviewQueueBindingIssueCode, type ReviewQueueExtractionAttestation, type ReviewQueueExtractionIssue, type ReviewQueueExtractionIssueCode, type ValidateReviewQueueBindingOptions, } from "./queue-binding.js";
8
9
  export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, type ExtractionAlignmentState, type ArtifactUnavailableCode, type BuiltExtractionInspectorCandidate, type BuiltExtractionInspectorModel, type ExtractionInspectorCandidate, type ExtractionInspectorEntry, type ExtractionInspectorExportOptions, type ExtractionInspectorFilters, type ExtractionInspectorInput, type ExtractionInspectorMountOptions, type ExtractionInspectorModel, type ExtractionInspectorSource, type ResolvedExtractionArtifact, } from "./extraction-inspector.js";
9
- export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, conflictSelectionIssue, unchosenProposedCandidates, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
10
+ export { renderFieldStatesHtml } from "./field-state-view.js";
11
+ export { drawRandomAuditSample, openRandomAuditSession, type RandomAuditSampleOptions, } from "./audit-sample.js";
12
+ export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, conflictSelectionIssue, unchosenProposedCandidates, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, isScoreBlind, assertReviewSessionConditions, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewSessionPresentation, type ReviewSessionSampling, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
10
13
  export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ExcludedProposalPresentation, type ExcludedProposalsUnreadable, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
11
14
  export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
12
15
  export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, type ReviewSessionReplayWarning, } from "./review-session-replay.js";
@@ -109,6 +112,13 @@ export interface MountReviewWorkbenchOptions {
109
112
  readonly extractionImport?: ExtractionEnvelopeImport | ExtractionEnvelopeImportResult;
110
113
  /** Maximum review cards mounted at once. Defaults to 50 and is capped at 250. */
111
114
  readonly pageSize?: number;
115
+ /**
116
+ * The queue's derived field states (`deriveFieldStates`). When supplied, a
117
+ * field-state panel lists every field with its content state, lifecycle and
118
+ * signals, including fields that have no review card because they were not
119
+ * read. Lifecycles of queued fields follow the live session.
120
+ */
121
+ readonly fieldStates?: readonly FieldState[];
112
122
  }
113
123
  export type ReviewQueueDisposition = "all" | "unresolved" | "accepted" | "kept" | "rejected" | "could-not-confirm";
114
124
  export interface ReviewQueueView {
@@ -230,6 +240,8 @@ export declare function renderReviewWorkbenchHtml(state: ReviewWorkbenchState |
230
240
  readonly queueView?: Partial<ReviewQueueView>;
231
241
  /** See {@link MountReviewWorkbenchOptions.extractionImport}. */
232
242
  readonly extractionImport?: ExtractionEnvelopeImport | ExtractionEnvelopeImportResult;
243
+ /** See {@link MountReviewWorkbenchOptions.fieldStates}. */
244
+ readonly fieldStates?: readonly FieldState[];
233
245
  }): string;
234
246
  export declare function buildReviewQueueWindow(session: ReviewQueueSessionState, view?: Partial<ReviewQueueView>): ReviewQueueWindow;
235
247
  /**
@@ -1,6 +1,6 @@
1
1
  import { canonicalJson } from "./canonical.js";
2
2
  import { assertReviewResolutionConsistency } from "../producer-discipline.js";
3
- import { candidateForDecision, conflictSelectionIssue, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, unchosenProposedCandidates, 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";
@@ -9,11 +9,14 @@ import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review
9
9
  import { attestReviewQueueExtraction } from "./queue-binding.js";
10
10
  import { candidateVerificationNotes, excludedProposalsSentence, excludedProposalsUnreadableSentence } from "./review-presentation.js";
11
11
  import { editedValueFromEditorText, isIsoCalendarDate, parsePlainDecimal } from "./edited-value.js";
12
+ import { renderFieldStatesHtml } from "./field-state-view.js";
12
13
  import { createAuditFactTrace, } from "./audit-rows.js";
13
14
  export { reviewAuditRowKeys } from "./audit-rows.js";
14
15
  export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, attestReviewQueueExtraction, bindReviewQueue, reviewItemCarriesExtractionBinding, unverifiedExtractionQueueSentence, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
15
16
  export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
16
- export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, conflictSelectionIssue, unchosenProposedCandidates, 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";
17
20
  export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
18
21
  export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
19
22
  export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
@@ -24,6 +27,7 @@ export function buildReviewDecision(state, options = {}) {
24
27
  return undefined;
25
28
  }
26
29
  const definition = workbenchDecisionDefinitions[state.decision];
30
+ assertReviewSessionConditions(state);
27
31
  if (state.decision === "could-not-confirm" && !state.note.trim()) {
28
32
  throw new Error("Could not confirm requires a non-empty reason.");
29
33
  }
@@ -70,6 +74,10 @@ export function buildReviewDecision(state, options = {}) {
70
74
  : {}),
71
75
  // A choice between conflicting values records the rivals it passed over.
72
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 } } : {}),
73
81
  },
74
82
  status: {
75
83
  ...(candidateProjection?.claimId ? { appliedToClaimIds: [candidateProjection.claimId] } : {}),
@@ -140,7 +148,8 @@ function decisionCardRenderedPrompt(state, presentation, adapter) {
140
148
  const excluded = excludedProposalsSentence(presentation.excludedProposals);
141
149
  const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
142
150
  // Likewise the verifier records the card shows, read against the same value.
143
- const verification = candidateVerificationNotes(state.item, state.decision === "accept-proposed" ? state.editedValue : undefined).map((note) => note.sentence);
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);
144
153
  return [decisionCardBasePrompt(state, presentation.targetLabel, adapter), ...(excluded ? [excluded] : []), ...(unreadable ? [unreadable] : []), ...verification].join(" ");
145
154
  }
146
155
  function decisionCardBasePrompt(state, targetLabel, adapter) {
@@ -514,7 +523,7 @@ export function createPersistentReviewSessionEventStore(options) {
514
523
  }
515
524
  export function renderReviewWorkbenchHtml(state, _events, options = {}) {
516
525
  const session = queueSessionFromStartState(state);
517
- return renderReviewQueueSessionHtml(session, options.presentationAdapter, normalizeReviewQueueView(options.queueView), attestReviewQueueExtraction(session.items, options.extractionImport));
526
+ return renderReviewQueueSessionHtml(session, options.presentationAdapter, normalizeReviewQueueView(options.queueView), attestReviewQueueExtraction(session.items, options.extractionImport), options.fieldStates);
518
527
  }
519
528
  /**
520
529
  * The visual/decision state of a field card, derived from the reviewer's local
@@ -602,7 +611,7 @@ function isEmptyValue(value) {
602
611
  }
603
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>";
604
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>";
605
- function renderReviewQueueSessionHtml(session, presentationAdapter, queueView, attestation) {
614
+ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView, attestation, fieldStates) {
606
615
  // A queue that does not match its stored extraction import is not reviewed:
607
616
  // its items could hide or alter what the source said.
608
617
  if (attestation.state === "diverges") {
@@ -625,6 +634,9 @@ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView, a
625
634
  ${attestation.state === "unverified"
626
635
  ? `<div class="queue-attestation unverified" data-testid="queue-attestation" data-state="unverified" role="note">${WARNING_SVG}<span>${escapeHtml(attestation.message)}</span></div>`
627
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
+ : ""}
628
640
  <header class="rhead">
629
641
  <div class="top">
630
642
  <div class="subj">
@@ -641,6 +653,7 @@ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView, a
641
653
  <button class="apply" type="button" data-testid="apply-button"${decidedCount === 0 ? " disabled" : ""}>Apply ${decidedCount} decision${decidedCount === 1 ? "" : "s"}</button>
642
654
  </div>
643
655
  </header>
656
+ ${fieldStates ? renderFieldStatesHtml(fieldStates, session) : ""}
644
657
  <nav class="queue-controls" aria-label="Review queue navigation">
645
658
  <label>
646
659
  <span>Find fields</span>
@@ -730,9 +743,9 @@ function renderFieldCard(item, session, presentationAdapter) {
730
743
  : proposedCandidates.length > 1
731
744
  ? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel, decision, session.selectedCandidateIdsByItemName?.[item.metadata.name])
732
745
  : "<p class=\"field-value\">No proposed value is available for this field.</p>"}
733
- ${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
746
+ ${proposed ? renderProvenanceRow(item, proposed, presentationAdapter, isScoreBlind(session)) : ""}
734
747
  ${renderExtractionImportNotes(presentation)}
735
- ${renderVerificationNotes(candidateVerificationNotes(item, decision === "accept-proposed" ? editedValue : undefined))}
748
+ ${isScoreBlind(session) ? "" : renderVerificationNotes(candidateVerificationNotes(item, decision === "accept-proposed" ? editedValue : undefined))}
736
749
  <div class="decide">
737
750
  ${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
738
751
  ${proposed ? `<button class="btn use" type="button" data-testid="use-proposed" data-item-name="${escapeHtml(item.metadata.name)}">Use proposed</button>
@@ -955,7 +968,7 @@ export function validateProposedValue(descriptor, rawValue) {
955
968
  return undefined;
956
969
  }
957
970
  }
958
- function renderProvenanceRow(item, proposed, presentationAdapter) {
971
+ function renderProvenanceRow(item, proposed, presentationAdapter, scoreBlind) {
959
972
  const excerpt = proposed.locator?.excerpt;
960
973
  if (!excerpt) {
961
974
  return `
@@ -968,7 +981,8 @@ function renderProvenanceRow(item, proposed, presentationAdapter) {
968
981
  </div>
969
982
  `;
970
983
  }
971
- 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;
972
986
  const presentation = buildReviewCandidatePresentation(item, proposed, presentationAdapter);
973
987
  const sourceLinkHtml = presentation.sourceLink
974
988
  ? `<a href="${escapeHtml(presentation.sourceLink.href)}">${escapeHtml(presentation.sourceLink.label ?? presentation.sourceText)}</a>`
@@ -1064,6 +1078,8 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
1064
1078
  selectedCandidateId: session.selectedCandidateIdsByItemName?.[item.metadata.name],
1065
1079
  reviewedAt: session.reviewedAt,
1066
1080
  actorId: session.actorId,
1081
+ ...(session.presentation !== undefined ? { presentation: session.presentation } : {}),
1082
+ ...(session.sampling !== undefined ? { sampling: session.sampling } : {}),
1067
1083
  };
1068
1084
  const reviewDecisionPayload = buildReviewDecision(state, { presentationAdapter });
1069
1085
  const preview = buildSurfaceProjectionPreview(item, reviewDecisionPayload, presentationAdapter);
@@ -1348,10 +1364,14 @@ function createReviewWorkbenchController(root, startState, options) {
1348
1364
  const attemptEvidenceIds = itemName && decision === "could-not-confirm"
1349
1365
  ? session.attemptEvidenceIdsByItemName?.[itemName]
1350
1366
  : undefined;
1367
+ const conditions = sessionConditionsStamp(session);
1351
1368
  const defaultData = decision
1352
- ? (editedValue !== undefined
1353
- ? { workbenchDecision: decision, workbenchEditedValue: editedValue }
1354
- : { 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
+ }
1355
1375
  : undefined;
1356
1376
  return buildReviewSessionEvent(session, {
1357
1377
  sessionName: defaultReviewSessionName,
@@ -1439,7 +1459,7 @@ function createReviewWorkbenchController(root, startState, options) {
1439
1459
  currentSession: () => session,
1440
1460
  currentSessionExport: () => buildReviewWorkbenchSessionExport(session, events, { presentationAdapter: options.presentationAdapter }),
1441
1461
  presentationAdapter: options.presentationAdapter,
1442
- renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter, queueView, attestation),
1462
+ renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter, queueView, attestation, options.fieldStates),
1443
1463
  setDecision,
1444
1464
  clearDecision,
1445
1465
  updateReviewerNote,
@@ -1459,8 +1479,8 @@ function reviewSessionStorageKey(session, keyPrefix) {
1459
1479
  const itemNames = buildReviewSessionResource(session).spec.reviewItemNames.join(",");
1460
1480
  return `${keyPrefix}:${itemNames}`;
1461
1481
  }
1462
- function renderCurrentState(root, session, controller, presentationAdapter, queueView, attestation) {
1463
- root.innerHTML = renderReviewQueueSessionHtml(session, presentationAdapter, queueView ?? normalizeReviewQueueView(undefined), attestation ?? attestReviewQueueExtraction(session.items, undefined));
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);
1464
1484
  bindFieldCardInteractions(root, controller);
1465
1485
  bindApplyButton(root, controller);
1466
1486
  bindClampToggles(root);
@@ -1513,8 +1533,14 @@ function queueSessionFromStartState(startState) {
1513
1533
  : {}),
1514
1534
  reviewedAt: startState.reviewedAt,
1515
1535
  actorId: startState.actorId,
1536
+ ...(startState.presentation !== undefined ? { presentation: startState.presentation } : {}),
1537
+ ...(startState.sampling !== undefined ? { sampling: startState.sampling } : {}),
1516
1538
  };
1517
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
+ }
1518
1544
  const COULD_NOT_CONFIRM_REASON_REQUIRED = "A reason is required when you could not confirm.";
1519
1545
  /**
1520
1546
  * Opens every collapsed region between `control` and the card before anything