@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.
- package/dist/src/calibration.d.ts +15 -0
- package/dist/src/calibration.js +11 -0
- package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
- package/dist/src/canonical-reviewed-trust-input.js +45 -1
- package/dist/src/console/review-console-server.js +22 -4
- package/dist/src/decision-carry-forward.d.ts +157 -0
- package/dist/src/decision-carry-forward.js +295 -0
- package/dist/src/extraction-envelope.d.ts +4 -3
- package/dist/src/extraction-envelope.js +3 -3
- package/dist/src/field-states.d.ts +100 -0
- package/dist/src/field-states.js +169 -0
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +3 -1
- package/dist/src/mcp/review-mcp.js +173 -45
- package/dist/src/review-resource.d.ts +36 -0
- package/dist/src/review-session-file.d.ts +7 -0
- package/dist/src/review-workbench/audit-rows.d.ts +1 -1
- package/dist/src/review-workbench/audit-rows.js +1 -0
- package/dist/src/review-workbench/audit-sample.d.ts +32 -0
- package/dist/src/review-workbench/audit-sample.js +53 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
- package/dist/src/review-workbench/extraction-inspector.js +1 -1
- package/dist/src/review-workbench/field-state-view.d.ts +15 -0
- package/dist/src/review-workbench/field-state-view.js +84 -0
- package/dist/src/review-workbench/queue-binding.d.ts +45 -2
- package/dist/src/review-workbench/queue-binding.js +45 -0
- package/dist/src/review-workbench/review-presentation.d.ts +23 -0
- package/dist/src/review-workbench/review-presentation.js +51 -10
- package/dist/src/review-workbench/review-queue-session.d.ts +93 -5
- package/dist/src/review-workbench/review-queue-session.js +147 -11
- package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
- package/dist/src/review-workbench/review-session-replay.js +49 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +126 -0
- package/dist/src/review-workbench/review-workbench-element.d.ts +10 -0
- package/dist/src/review-workbench/review-workbench-element.js +21 -3
- package/dist/src/review-workbench/review-workbench.css +126 -0
- package/dist/src/review-workbench/review-workbench.d.ts +40 -8
- package/dist/src/review-workbench/review-workbench.js +192 -71
- package/dist/src/review-workbench/review-workbench.standalone.css +126 -0
- package/dist/src/review-workbench/server-review-session.d.ts +18 -0
- package/dist/src/review-workbench/server-review-session.js +20 -2
- package/dist/src/surface-reviewed-extraction.d.ts +23 -0
- package/dist/src/surface-reviewed-extraction.js +64 -1
- package/dist/src/to-surface.js +32 -3
- 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 {
|
|
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 {
|
|
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
|
|
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
|
-
*
|
|
125
|
-
*
|
|
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,
|
|
132
|
-
// The card
|
|
133
|
-
//
|
|
134
|
-
const excluded = excludedProposalsSentence(
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
143
|
-
// not confirm; the prompt states the same
|
|
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
|
|
172
|
+
return `${values} Selected decision: ${decisionLabel}.`;
|
|
149
173
|
}
|
|
150
174
|
const proposedCandidate = proposedCandidates[0];
|
|
151
|
-
const currentValue =
|
|
152
|
-
const proposedValue =
|
|
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
|
|
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
|
-
|
|
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.
|
|
658
|
-
//
|
|
659
|
-
//
|
|
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
|
|
736
|
-
*
|
|
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
|
-
|
|
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>${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
?
|
|
1288
|
-
|
|
1289
|
-
|
|
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
|
-
|
|
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 =
|
|
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) => {
|