@kontourai/survey 6.1.0 → 7.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/example-data/public-directory-review-resource.d.ts +3 -3
- package/dist/src/canonical-reviewed-trust-input.js +30 -1
- package/dist/src/console/review-console-server.js +22 -4
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/review-mcp.js +139 -36
- package/dist/src/review-resource.d.ts +7 -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/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 +47 -4
- package/dist/src/review-workbench/review-queue-session.js +73 -8
- package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
- package/dist/src/review-workbench/review-session-replay.js +35 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +94 -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 +94 -0
- package/dist/src/review-workbench/review-workbench.d.ts +28 -8
- package/dist/src/review-workbench/review-workbench.js +157 -62
- package/dist/src/review-workbench/review-workbench.standalone.css +94 -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,24 +1,25 @@
|
|
|
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, 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";
|
|
11
12
|
import { createAuditFactTrace, } from "./audit-rows.js";
|
|
12
13
|
export { reviewAuditRowKeys } from "./audit-rows.js";
|
|
13
|
-
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
|
|
14
|
+
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, attestReviewQueueExtraction, bindReviewQueue, reviewItemCarriesExtractionBinding, unverifiedExtractionQueueSentence, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
|
|
14
15
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
15
|
-
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
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";
|
|
16
17
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
17
18
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
18
19
|
export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
19
20
|
export { checkEditedValueForItem, editedValueFromEditorText, } from "./edited-value.js";
|
|
20
21
|
export { assertReviewDecisionModeAllows, DecisionModeViolationError, validateReviewDecisionMode, } from "./producer-decision-mode.js";
|
|
21
|
-
export function buildReviewDecision(state) {
|
|
22
|
+
export function buildReviewDecision(state, options = {}) {
|
|
22
23
|
if (!state.decision) {
|
|
23
24
|
return undefined;
|
|
24
25
|
}
|
|
@@ -26,7 +27,8 @@ export function buildReviewDecision(state) {
|
|
|
26
27
|
if (state.decision === "could-not-confirm" && !state.note.trim()) {
|
|
27
28
|
throw new Error("Could not confirm requires a non-empty reason.");
|
|
28
29
|
}
|
|
29
|
-
const candidate = candidateForDecision(state.item, state.decision);
|
|
30
|
+
const candidate = candidateForDecision(state.item, state.decision, state.selectedCandidateId);
|
|
31
|
+
const unchosen = unchosenProposedCandidates(state.item, state.decision, state.selectedCandidateId);
|
|
30
32
|
// A decision that selects no candidate (a value-neutral decision on a
|
|
31
33
|
// conflict) records no candidate id and no candidate's projection hints.
|
|
32
34
|
const selectsNone = decisionSelectsNoCandidate(state.item, state.decision);
|
|
@@ -36,7 +38,7 @@ export function buildReviewDecision(state) {
|
|
|
36
38
|
?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
|
|
37
39
|
};
|
|
38
40
|
const candidateProjection = selectsNone ? undefined : candidate.projection;
|
|
39
|
-
const authorizing = buildDecisionCardAuthorizing(state);
|
|
41
|
+
const authorizing = buildDecisionCardAuthorizing(state, options.presentationAdapter);
|
|
40
42
|
const reviewDecision = {
|
|
41
43
|
apiVersion: reviewResourceApiVersion,
|
|
42
44
|
kind: "ReviewDecision",
|
|
@@ -66,6 +68,8 @@ export function buildReviewDecision(state) {
|
|
|
66
68
|
...(state.decision === "accept-proposed" && state.editedValue !== undefined
|
|
67
69
|
? { editedValue: state.editedValue }
|
|
68
70
|
: {}),
|
|
71
|
+
// A choice between conflicting values records the rivals it passed over.
|
|
72
|
+
...(unchosen.length ? { unselectedCandidateIds: unchosen.map((entry) => entry.id) } : {}),
|
|
69
73
|
},
|
|
70
74
|
status: {
|
|
71
75
|
...(candidateProjection?.claimId ? { appliedToClaimIds: [candidateProjection.claimId] } : {}),
|
|
@@ -91,12 +95,11 @@ const DECISION_CARD_PROMPT_REF = "review-workbench/decision-card@v1";
|
|
|
91
95
|
* On validation failure, emits a console warning and returns undefined so the
|
|
92
96
|
* outcome is recorded without authorizing (transparency-gap-not-blocker per ADR 0004).
|
|
93
97
|
*/
|
|
94
|
-
function buildDecisionCardAuthorizing(state) {
|
|
98
|
+
function buildDecisionCardAuthorizing(state, adapter) {
|
|
95
99
|
if (!state.decision) {
|
|
96
100
|
return undefined;
|
|
97
101
|
}
|
|
98
|
-
const
|
|
99
|
-
const renderedPrompt = decisionCardRenderedPrompt(state, targetLabel);
|
|
102
|
+
const renderedPrompt = decisionCardRenderedPrompt(state, buildReviewItemPresentation(state.item, adapter), adapter);
|
|
100
103
|
// Reviewer note present means the reviewer also typed a rationale — "typed".
|
|
101
104
|
// Control-only affirmation (no note) is "affirmed-control".
|
|
102
105
|
const action = state.note?.trim() ? "typed" : "affirmed-control";
|
|
@@ -121,35 +124,47 @@ function buildDecisionCardAuthorizing(state) {
|
|
|
121
124
|
}
|
|
122
125
|
}
|
|
123
126
|
/**
|
|
124
|
-
*
|
|
125
|
-
*
|
|
127
|
+
* Renders the decision card's prompt for a review item and decision.
|
|
128
|
+
*
|
|
129
|
+
* It is rebuilt from the item when the decision is built, not captured from
|
|
130
|
+
* what a reviewer's screen showed: it records what the card states for this
|
|
131
|
+
* item, with the same presentation adapter, target label and value text.
|
|
126
132
|
*
|
|
127
133
|
* Format mirrors the review question shown in the workbench header:
|
|
128
134
|
* "For {target}, decide whether {proposed} should replace {current}."
|
|
129
135
|
* followed by the selected decision label so the block is self-contained.
|
|
130
136
|
*/
|
|
131
|
-
function decisionCardRenderedPrompt(state,
|
|
132
|
-
// The card
|
|
133
|
-
//
|
|
134
|
-
const excluded = excludedProposalsSentence(
|
|
135
|
-
|
|
137
|
+
function decisionCardRenderedPrompt(state, presentation, adapter) {
|
|
138
|
+
// The card states proposals excluded at import, and stored ones it cannot
|
|
139
|
+
// show; so does the prompt.
|
|
140
|
+
const excluded = excludedProposalsSentence(presentation.excludedProposals);
|
|
141
|
+
const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
|
|
142
|
+
// Likewise the verifier records the card shows, read against the same value.
|
|
136
143
|
const verification = candidateVerificationNotes(state.item, state.decision === "accept-proposed" ? state.editedValue : undefined).map((note) => note.sentence);
|
|
137
|
-
return [decisionCardBasePrompt(state, targetLabel), ...(excluded ? [excluded] : []), ...verification].join(" ");
|
|
144
|
+
return [decisionCardBasePrompt(state, presentation.targetLabel, adapter), ...(excluded ? [excluded] : []), ...(unreadable ? [unreadable] : []), ...verification].join(" ");
|
|
138
145
|
}
|
|
139
|
-
function decisionCardBasePrompt(state, targetLabel) {
|
|
146
|
+
function decisionCardBasePrompt(state, targetLabel, adapter) {
|
|
147
|
+
const valueText = (candidate) => buildReviewCandidatePresentation(state.item, candidate, adapter, targetLabel).valueText;
|
|
140
148
|
const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
|
|
141
149
|
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
|
|
150
|
+
// A conflict card lists every proposed value and offers a choice of one,
|
|
151
|
+
// reject-all or could not confirm; the prompt states the same: every value,
|
|
152
|
+
// and for a choice, which value was chosen and which were not.
|
|
144
153
|
if (proposedCandidates.length > 1) {
|
|
154
|
+
const values = `For ${targetLabel}, ${proposedCandidates.length} different values were proposed: ${proposedCandidates.map(valueText).join(", ")}.`;
|
|
155
|
+
if (state.decision === "select-proposed") {
|
|
156
|
+
const chosen = proposedCandidates.find((c) => c.id === state.selectedCandidateId);
|
|
157
|
+
const unchosen = proposedCandidates.filter((c) => c.id !== state.selectedCandidateId);
|
|
158
|
+
return `${values} Selected decision: ${workbenchDecisionDefinitions[state.decision].label}: ${chosen ? valueText(chosen) : formatValue("")} (not chosen: ${unchosen.map(valueText).join(", ")}).`;
|
|
159
|
+
}
|
|
145
160
|
const decisionLabel = state.decision === "reject-proposed"
|
|
146
161
|
? "Reject all values"
|
|
147
162
|
: state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
148
|
-
return
|
|
163
|
+
return `${values} Selected decision: ${decisionLabel}.`;
|
|
149
164
|
}
|
|
150
165
|
const proposedCandidate = proposedCandidates[0];
|
|
151
|
-
const currentValue =
|
|
152
|
-
const proposedValue =
|
|
166
|
+
const currentValue = currentCandidate ? valueText(currentCandidate) : formatValue("");
|
|
167
|
+
const proposedValue = proposedCandidate ? valueText(proposedCandidate) : formatValue("");
|
|
153
168
|
const decisionLabel = state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
154
169
|
return `For ${targetLabel}, decide whether ${proposedValue} should replace ${currentValue}. Selected decision: ${decisionLabel}.`;
|
|
155
170
|
}
|
|
@@ -161,7 +176,7 @@ export class ReviewApplyActionMappingError extends Error {
|
|
|
161
176
|
this.issues = issues;
|
|
162
177
|
}
|
|
163
178
|
}
|
|
164
|
-
export function buildReviewDecisionsFromSession(session) {
|
|
179
|
+
export function buildReviewDecisionsFromSession(session, options = {}) {
|
|
165
180
|
return session.items.flatMap((item) => {
|
|
166
181
|
const decision = session.decisionsByItemName[item.metadata.name];
|
|
167
182
|
if (!decision) {
|
|
@@ -170,7 +185,7 @@ export function buildReviewDecisionsFromSession(session) {
|
|
|
170
185
|
const reviewDecision = buildReviewDecision(currentReviewWorkbenchState({
|
|
171
186
|
...session,
|
|
172
187
|
activeItemName: item.metadata.name,
|
|
173
|
-
}));
|
|
188
|
+
}), options);
|
|
174
189
|
return reviewDecision ? [reviewDecision] : [];
|
|
175
190
|
});
|
|
176
191
|
}
|
|
@@ -291,7 +306,7 @@ function matchingSelectedCandidate(item, result) {
|
|
|
291
306
|
function structuralEqual(left, right) {
|
|
292
307
|
return canonicalJson(left) === canonicalJson(right);
|
|
293
308
|
}
|
|
294
|
-
export function buildReviewWorkbenchResultsFromSession(session) {
|
|
309
|
+
export function buildReviewWorkbenchResultsFromSession(session, options = {}) {
|
|
295
310
|
return session.items.flatMap((item) => {
|
|
296
311
|
const decision = session.decisionsByItemName[item.metadata.name];
|
|
297
312
|
if (!decision) {
|
|
@@ -301,7 +316,7 @@ export function buildReviewWorkbenchResultsFromSession(session) {
|
|
|
301
316
|
...session,
|
|
302
317
|
activeItemName: item.metadata.name,
|
|
303
318
|
});
|
|
304
|
-
const reviewDecision = buildReviewDecision(state);
|
|
319
|
+
const reviewDecision = buildReviewDecision(state, options);
|
|
305
320
|
if (!reviewDecision) {
|
|
306
321
|
return [];
|
|
307
322
|
}
|
|
@@ -315,11 +330,12 @@ export function buildReviewWorkbenchResultsFromSession(session) {
|
|
|
315
330
|
rationale: reviewDecision.spec.rationale,
|
|
316
331
|
}];
|
|
317
332
|
}
|
|
318
|
-
const
|
|
333
|
+
const selectedCandidateId = session.selectedCandidateIdsByItemName?.[item.metadata.name];
|
|
334
|
+
const selectedCandidate = candidateForDecision(item, decision, selectedCandidateId);
|
|
319
335
|
const editedValue = decision === "accept-proposed"
|
|
320
336
|
? session.editedValuesByItemName?.[item.metadata.name]
|
|
321
337
|
: undefined;
|
|
322
|
-
const effectiveValue = effectiveValueForDecision(item, decision, editedValue);
|
|
338
|
+
const effectiveValue = effectiveValueForDecision(item, decision, editedValue, selectedCandidateId);
|
|
323
339
|
return [{
|
|
324
340
|
reviewItemName: item.metadata.name,
|
|
325
341
|
decision,
|
|
@@ -338,12 +354,12 @@ export function buildReviewWorkbenchResultsFromSession(session) {
|
|
|
338
354
|
}];
|
|
339
355
|
});
|
|
340
356
|
}
|
|
341
|
-
export function buildReviewWorkbenchSessionExport(session, events = buildReviewSessionEvents(session)) {
|
|
357
|
+
export function buildReviewWorkbenchSessionExport(session, events = buildReviewSessionEvents(session), options = {}) {
|
|
342
358
|
return {
|
|
343
359
|
session: buildReviewSessionResource(session, events),
|
|
344
360
|
events,
|
|
345
|
-
decisions: buildReviewDecisionsFromSession(session),
|
|
346
|
-
results: buildReviewWorkbenchResultsFromSession(session),
|
|
361
|
+
decisions: buildReviewDecisionsFromSession(session, options),
|
|
362
|
+
results: buildReviewWorkbenchResultsFromSession(session, options),
|
|
347
363
|
};
|
|
348
364
|
}
|
|
349
365
|
export function replayReviewSessionEventsForSnapshot(snapshot, events) {
|
|
@@ -353,9 +369,9 @@ export function replayReviewSessionEventsForSnapshot(snapshot, events) {
|
|
|
353
369
|
}
|
|
354
370
|
return replayReviewSessionEvents(snapshot, events);
|
|
355
371
|
}
|
|
356
|
-
export function buildReviewWorkbenchSessionExportForSnapshot(snapshot, events) {
|
|
372
|
+
export function buildReviewWorkbenchSessionExportForSnapshot(snapshot, events, options = {}) {
|
|
357
373
|
const replayedSession = replayReviewSessionEventsForSnapshot(snapshot, events);
|
|
358
|
-
return buildReviewWorkbenchSessionExport(replayedSession, events);
|
|
374
|
+
return buildReviewWorkbenchSessionExport(replayedSession, events, options);
|
|
359
375
|
}
|
|
360
376
|
export function deriveReviewSessionApplyResultForSnapshot(options) {
|
|
361
377
|
const requiredResolvedItems = options.requiredResolvedItems ?? "none";
|
|
@@ -371,7 +387,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
|
|
|
371
387
|
}
|
|
372
388
|
const warnings = reviewSessionReplayWarningsForSnapshot(options.snapshot, options.events);
|
|
373
389
|
const replayedSession = replayReviewSessionEvents(options.snapshot, options.events);
|
|
374
|
-
const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events);
|
|
390
|
+
const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events, { presentationAdapter: options.presentationAdapter });
|
|
375
391
|
const resolvedItemNames = new Set(sessionExport.results.map((result) => result.reviewItemName));
|
|
376
392
|
const unresolvedItemNames = options.snapshot.items
|
|
377
393
|
.map((item) => item.metadata.name)
|
|
@@ -498,7 +514,7 @@ export function createPersistentReviewSessionEventStore(options) {
|
|
|
498
514
|
}
|
|
499
515
|
export function renderReviewWorkbenchHtml(state, _events, options = {}) {
|
|
500
516
|
const session = queueSessionFromStartState(state);
|
|
501
|
-
return renderReviewQueueSessionHtml(session, options.presentationAdapter, normalizeReviewQueueView(options.queueView));
|
|
517
|
+
return renderReviewQueueSessionHtml(session, options.presentationAdapter, normalizeReviewQueueView(options.queueView), attestReviewQueueExtraction(session.items, options.extractionImport));
|
|
502
518
|
}
|
|
503
519
|
/**
|
|
504
520
|
* The visual/decision state of a field card, derived from the reviewer's local
|
|
@@ -506,7 +522,7 @@ export function renderReviewWorkbenchHtml(state, _events, options = {}) {
|
|
|
506
522
|
* candidate set (e.g. a pre-decided item seeded by the host) as already-kept.
|
|
507
523
|
*/
|
|
508
524
|
function fieldCardState(item, decision) {
|
|
509
|
-
if (decision === "accept-proposed")
|
|
525
|
+
if (decision === "accept-proposed" || decision === "select-proposed")
|
|
510
526
|
return "accepted";
|
|
511
527
|
if (decision === "keep-current")
|
|
512
528
|
return "kept";
|
|
@@ -528,7 +544,9 @@ function fieldCardState(item, decision) {
|
|
|
528
544
|
*/
|
|
529
545
|
function chipLabel(state, hasCurrentValue, conflictingValues) {
|
|
530
546
|
switch (state) {
|
|
531
|
-
|
|
547
|
+
// On a conflict the only accepting decision is a choice of one value, and
|
|
548
|
+
// the chip says it was a choice, never a plain accept.
|
|
549
|
+
case "accepted": return conflictingValues ? `Chose 1 of ${conflictingValues} values` : "Accepted";
|
|
532
550
|
case "kept": return hasCurrentValue ? "Kept current" : conflictingValues ? "All values rejected" : "Left unset";
|
|
533
551
|
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : conflictingValues ? "All values rejected" : "Left unset";
|
|
534
552
|
case "could-not-confirm": return "Could not confirm";
|
|
@@ -584,7 +602,16 @@ function isEmptyValue(value) {
|
|
|
584
602
|
}
|
|
585
603
|
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
604
|
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) {
|
|
605
|
+
function renderReviewQueueSessionHtml(session, presentationAdapter, queueView, attestation) {
|
|
606
|
+
// A queue that does not match its stored extraction import is not reviewed:
|
|
607
|
+
// its items could hide or alter what the source said.
|
|
608
|
+
if (attestation.state === "diverges") {
|
|
609
|
+
return `
|
|
610
|
+
<section class="workbench-shell review" data-testid="review-workbench-shell" data-queue-attestation="diverges" aria-label="Survey review workbench">
|
|
611
|
+
<div class="queue-attestation diverges" data-testid="queue-attestation" data-state="diverges" role="alert">${WARNING_SVG}<span>${escapeHtml(attestation.message)}</span></div>
|
|
612
|
+
</section>
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
588
615
|
const totalCount = session.items.length;
|
|
589
616
|
const decidedCount = session.items.filter((item) => session.decisionsByItemName[item.metadata.name] !== undefined
|
|
590
617
|
|| item.spec.candidateSetStatus === "resolved").length;
|
|
@@ -594,7 +621,10 @@ function renderReviewQueueSessionHtml(session, presentationAdapter, queueView) {
|
|
|
594
621
|
const headerTitle = typeof producerName === "string" && producerName.length > 0 ? producerName : "Review queue";
|
|
595
622
|
const window = buildReviewQueueWindow(session, queueView);
|
|
596
623
|
return `
|
|
597
|
-
<section class="workbench-shell review" data-testid="review-workbench-shell" aria-label="Survey review workbench">
|
|
624
|
+
<section class="workbench-shell review" data-testid="review-workbench-shell" data-queue-attestation="${attestation.state}" aria-label="Survey review workbench">
|
|
625
|
+
${attestation.state === "unverified"
|
|
626
|
+
? `<div class="queue-attestation unverified" data-testid="queue-attestation" data-state="unverified" role="note">${WARNING_SVG}<span>${escapeHtml(attestation.message)}</span></div>`
|
|
627
|
+
: ""}
|
|
598
628
|
<header class="rhead">
|
|
599
629
|
<div class="top">
|
|
600
630
|
<div class="subj">
|
|
@@ -654,9 +684,9 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
654
684
|
const decided = decision !== undefined;
|
|
655
685
|
const current = item.spec.candidates.find((candidate) => candidate.role === "current");
|
|
656
686
|
const proposedCandidates = item.spec.candidates.filter((candidate) => candidate.role === "proposed");
|
|
657
|
-
// Several proposed values for one claim are a conflict.
|
|
658
|
-
//
|
|
659
|
-
//
|
|
687
|
+
// Several proposed values for one claim are a conflict. The reviewer can
|
|
688
|
+
// choose one of them by candidate id (select-proposed), reject them all, or
|
|
689
|
+
// end the round as could-not-confirm; no role-based control may pick one.
|
|
660
690
|
const conflict = proposedCandidates.length > 1;
|
|
661
691
|
const proposed = proposedCandidates.length === 1 ? proposedCandidates[0] : undefined;
|
|
662
692
|
const presentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
@@ -698,7 +728,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
698
728
|
${proposed
|
|
699
729
|
? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
|
|
700
730
|
: proposedCandidates.length > 1
|
|
701
|
-
? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel)
|
|
731
|
+
? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel, decision, session.selectedCandidateIdsByItemName?.[item.metadata.name])
|
|
702
732
|
: "<p class=\"field-value\">No proposed value is available for this field.</p>"}
|
|
703
733
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
704
734
|
${renderExtractionImportNotes(presentation)}
|
|
@@ -732,18 +762,37 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
732
762
|
}
|
|
733
763
|
/**
|
|
734
764
|
* The card body for an item whose candidate set holds several proposed values.
|
|
735
|
-
* Lists every value with its excerpt
|
|
736
|
-
*
|
|
765
|
+
* Lists every value with its excerpt. Until the field is decided, each value
|
|
766
|
+
* has its own "Use this value" control, which records `select-proposed` with
|
|
767
|
+
* that candidate's id. Once a value is chosen, every value stays listed and is
|
|
768
|
+
* marked chosen or not chosen, so the card never reads as if there had been
|
|
769
|
+
* one value. Proposals excluded at import are not candidates: they are listed
|
|
770
|
+
* by {@link renderExtractionImportNotes} and cannot be chosen.
|
|
737
771
|
*/
|
|
738
|
-
function renderConflictingProposals(item, candidates, presentationAdapter, targetLabel) {
|
|
772
|
+
function renderConflictingProposals(item, candidates, presentationAdapter, targetLabel, decision, selectedCandidateId) {
|
|
773
|
+
const chose = decision === "select-proposed";
|
|
739
774
|
const values = candidates.map((candidate) => {
|
|
740
775
|
const text = buildReviewCandidatePresentation(item, candidate, presentationAdapter, targetLabel).valueText;
|
|
741
776
|
const excerpt = candidate.locator?.excerpt;
|
|
742
|
-
|
|
777
|
+
const chosen = chose && candidate.id === selectedCandidateId;
|
|
778
|
+
const mark = chose
|
|
779
|
+
? ` <span class="choice-tag ${chosen ? "chosen" : "not-chosen"}" data-testid="choice-tag">${chosen ? "Chosen" : "Not chosen"}</span>`
|
|
780
|
+
: "";
|
|
781
|
+
const control = decision === undefined
|
|
782
|
+
? `<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>`
|
|
783
|
+
: "";
|
|
784
|
+
return `<li data-testid="conflicting-value" data-candidate-id="${escapeHtml(candidate.id)}"${chose ? ` data-chosen="${chosen ? "true" : "false"}"` : ""}>
|
|
785
|
+
<span class="conflict-text"><span class="vtext">${escapeHtml(text)}</span>${excerpt ? ` <q>${escapeHtml(excerpt)}</q>` : ""}${mark}</span>${control}
|
|
786
|
+
</li>`;
|
|
743
787
|
}).join("");
|
|
788
|
+
const intro = decision === undefined
|
|
789
|
+
? `${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.`
|
|
790
|
+
: chose
|
|
791
|
+
? `${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.`
|
|
792
|
+
: `${candidates.length} different values were proposed for this field.`;
|
|
744
793
|
return `
|
|
745
|
-
<div class="field-value" data-testid="conflicting-proposals">
|
|
746
|
-
<p>${
|
|
794
|
+
<div class="field-value conflict-values" data-testid="conflicting-proposals">
|
|
795
|
+
<p>${escapeHtml(intro)}</p>
|
|
747
796
|
<ul>${values}</ul>
|
|
748
797
|
</div>
|
|
749
798
|
`;
|
|
@@ -944,7 +993,8 @@ function renderProvenanceRow(item, proposed, presentationAdapter) {
|
|
|
944
993
|
*/
|
|
945
994
|
function renderExtractionImportNotes(presentation) {
|
|
946
995
|
const { excerptVerification: verification, excludedProposals: excluded } = presentation;
|
|
947
|
-
|
|
996
|
+
const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
|
|
997
|
+
if (verification === undefined && excluded.length === 0 && unreadable === undefined)
|
|
948
998
|
return "";
|
|
949
999
|
const excludedHtml = excluded.length === 0 ? "" : `
|
|
950
1000
|
<div class="noprov" data-testid="excluded-proposals" role="note">
|
|
@@ -954,12 +1004,18 @@ function renderExtractionImportNotes(presentation) {
|
|
|
954
1004
|
${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
1005
|
Unverifiable is not disproven: check the source before deciding.</span>
|
|
956
1006
|
</div>`;
|
|
1007
|
+
const unreadableHtml = unreadable === undefined ? "" : `
|
|
1008
|
+
<div class="noprov" data-testid="excluded-proposals-unreadable" data-reason="${escapeHtml(presentation.excludedProposalsUnreadable.reason)}" role="note">
|
|
1009
|
+
${WARNING_SVG}
|
|
1010
|
+
<span class="tag">${presentation.excludedProposalsUnreadable.reason === "binding-broken" ? "Binding broken" : "Unreadable"}</span>
|
|
1011
|
+
<span>${escapeHtml(unreadable)} Check the source before deciding.</span>
|
|
1012
|
+
</div>`;
|
|
957
1013
|
const verificationHtml = verification === undefined ? "" : `
|
|
958
1014
|
<p class="excerpt-verification" data-testid="excerpt-verification" data-verified="${verification === "verified" ? "true" : "false"}">${verification === "verified"
|
|
959
1015
|
? "Excerpts checked against the prepared source text at import."
|
|
960
1016
|
: "Excerpts not checked against the prepared source text at import."}</p>`;
|
|
961
1017
|
return `
|
|
962
|
-
<div class="prov import-notes">${excludedHtml}${verificationHtml}
|
|
1018
|
+
<div class="prov import-notes">${excludedHtml}${unreadableHtml}${verificationHtml}
|
|
963
1019
|
</div>
|
|
964
1020
|
`;
|
|
965
1021
|
}
|
|
@@ -1005,10 +1061,11 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
1005
1061
|
note,
|
|
1006
1062
|
decision,
|
|
1007
1063
|
editedValue: session.editedValuesByItemName?.[item.metadata.name],
|
|
1064
|
+
selectedCandidateId: session.selectedCandidateIdsByItemName?.[item.metadata.name],
|
|
1008
1065
|
reviewedAt: session.reviewedAt,
|
|
1009
1066
|
actorId: session.actorId,
|
|
1010
1067
|
};
|
|
1011
|
-
const reviewDecisionPayload = buildReviewDecision(state);
|
|
1068
|
+
const reviewDecisionPayload = buildReviewDecision(state, { presentationAdapter });
|
|
1012
1069
|
const preview = buildSurfaceProjectionPreview(item, reviewDecisionPayload, presentationAdapter);
|
|
1013
1070
|
// Seeded with what the card face already shows, so the audit surface never
|
|
1014
1071
|
// reprints it. Passed down through every section: the sections render in DOM
|
|
@@ -1033,7 +1090,12 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
1033
1090
|
${proposed?.extraction.model ? fieldItem("model", "Model", proposed.extraction.model) : ""}
|
|
1034
1091
|
${proposed ? placementItem(trace, { of: proposed.id, property: "extraction.extractor" }, "extractor", "Extractor", proposed.extraction.extractor ?? "unknown") : ""}
|
|
1035
1092
|
${proposed ? fieldItem("extracted-at", "Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
|
|
1036
|
-
${
|
|
1093
|
+
${(() => {
|
|
1094
|
+
const itemPresentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
1095
|
+
const unreadable = excludedProposalsUnreadableSentence(itemPresentation.excludedProposalsUnreadable);
|
|
1096
|
+
return itemPresentation.excludedProposals.map((entry) => fieldItem("excluded-proposal", `Excluded proposal ${entry.proposalIndex}`, `${entry.valueText} at ${entry.locator}: excerpt not at the cited span`)).join("")
|
|
1097
|
+
+ (unreadable ? fieldItem("excluded-proposals-unreadable", "Excluded proposals not shown", unreadable) : "");
|
|
1098
|
+
})()}
|
|
1037
1099
|
</dl>
|
|
1038
1100
|
${preview
|
|
1039
1101
|
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview, trace)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
@@ -1257,6 +1319,9 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1257
1319
|
let events = eventStore?.load(baseSession) ?? [];
|
|
1258
1320
|
let session = events.length > 0 ? replayReviewSessionEvents(baseSession, events) : baseSession;
|
|
1259
1321
|
let queueView = normalizeReviewQueueView({ pageSize: options.pageSize });
|
|
1322
|
+
// The queue's items never change after mount (decisions live in events), so
|
|
1323
|
+
// the check against its extraction import runs once.
|
|
1324
|
+
const attestation = attestReviewQueueExtraction(baseSession.items, options.extractionImport);
|
|
1260
1325
|
const persistEvents = () => {
|
|
1261
1326
|
eventStore?.save(session, events);
|
|
1262
1327
|
};
|
|
@@ -1272,7 +1337,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1272
1337
|
const session = sessionForEvent;
|
|
1273
1338
|
const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
|
|
1274
1339
|
const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
|
|
1275
|
-
const candidateId = item && decision ? decisionCandidateId(item, decision) : undefined;
|
|
1340
|
+
const candidateId = item && decision ? decisionCandidateId(item, decision, session.selectedCandidateIdsByItemName?.[item.metadata.name]) : undefined;
|
|
1276
1341
|
const definition = decision ? workbenchDecisionDefinitions[decision] : undefined;
|
|
1277
1342
|
const note = itemName ? session.notesByItemName[itemName] : undefined;
|
|
1278
1343
|
// Carry the reviewer's inline edit in the event (accept-proposed only), so
|
|
@@ -1315,7 +1380,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1315
1380
|
events = [...events, event];
|
|
1316
1381
|
persistEvents();
|
|
1317
1382
|
};
|
|
1318
|
-
const setDecision = (itemName, decision, rawEditedValue) => {
|
|
1383
|
+
const setDecision = (itemName, decision, rawEditedValue, selectedCandidateId) => {
|
|
1319
1384
|
if (decision === "could-not-confirm" && !session.notesByItemName[itemName]?.trim()) {
|
|
1320
1385
|
throw new Error("Could not confirm requires a non-empty reason.");
|
|
1321
1386
|
}
|
|
@@ -1334,11 +1399,19 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1334
1399
|
else {
|
|
1335
1400
|
delete nextEditedValuesByItemName[itemName];
|
|
1336
1401
|
}
|
|
1402
|
+
const nextSelections = { ...session.selectedCandidateIdsByItemName };
|
|
1403
|
+
if (decision === "select-proposed" && selectedCandidateId !== undefined) {
|
|
1404
|
+
nextSelections[itemName] = selectedCandidateId;
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
delete nextSelections[itemName];
|
|
1408
|
+
}
|
|
1337
1409
|
commitSessionUpdate({
|
|
1338
1410
|
...session,
|
|
1339
1411
|
activeItemName: itemName,
|
|
1340
1412
|
decisionsByItemName: { ...session.decisionsByItemName, [itemName]: decision },
|
|
1341
1413
|
editedValuesByItemName: nextEditedValuesByItemName,
|
|
1414
|
+
selectedCandidateIdsByItemName: nextSelections,
|
|
1342
1415
|
}, "decision-changed", itemName);
|
|
1343
1416
|
};
|
|
1344
1417
|
const clearDecision = (itemName) => {
|
|
@@ -1348,12 +1421,15 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1348
1421
|
delete remainingEdits[itemName];
|
|
1349
1422
|
const remainingAttemptEvidenceIds = { ...session.attemptEvidenceIdsByItemName };
|
|
1350
1423
|
delete remainingAttemptEvidenceIds[itemName];
|
|
1424
|
+
const remainingSelections = { ...session.selectedCandidateIdsByItemName };
|
|
1425
|
+
delete remainingSelections[itemName];
|
|
1351
1426
|
commitSessionUpdate({
|
|
1352
1427
|
...session,
|
|
1353
1428
|
activeItemName: itemName,
|
|
1354
1429
|
decisionsByItemName: remainingDecisions,
|
|
1355
1430
|
editedValuesByItemName: remainingEdits,
|
|
1356
1431
|
attemptEvidenceIdsByItemName: remainingAttemptEvidenceIds,
|
|
1432
|
+
selectedCandidateIdsByItemName: remainingSelections,
|
|
1357
1433
|
}, "decision-changed", itemName, { workbenchDecision: null });
|
|
1358
1434
|
};
|
|
1359
1435
|
const updateReviewerNote = (itemName, note) => {
|
|
@@ -1361,8 +1437,9 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1361
1437
|
};
|
|
1362
1438
|
const controller = {
|
|
1363
1439
|
currentSession: () => session,
|
|
1364
|
-
currentSessionExport: () => buildReviewWorkbenchSessionExport(session, events),
|
|
1365
|
-
|
|
1440
|
+
currentSessionExport: () => buildReviewWorkbenchSessionExport(session, events, { presentationAdapter: options.presentationAdapter }),
|
|
1441
|
+
presentationAdapter: options.presentationAdapter,
|
|
1442
|
+
renderCurrentState: () => renderCurrentState(root, session, controller, options.presentationAdapter, queueView, attestation),
|
|
1366
1443
|
setDecision,
|
|
1367
1444
|
clearDecision,
|
|
1368
1445
|
updateReviewerNote,
|
|
@@ -1382,8 +1459,8 @@ function reviewSessionStorageKey(session, keyPrefix) {
|
|
|
1382
1459
|
const itemNames = buildReviewSessionResource(session).spec.reviewItemNames.join(",");
|
|
1383
1460
|
return `${keyPrefix}:${itemNames}`;
|
|
1384
1461
|
}
|
|
1385
|
-
function renderCurrentState(root, session, controller, presentationAdapter, queueView) {
|
|
1386
|
-
root.innerHTML =
|
|
1462
|
+
function renderCurrentState(root, session, controller, presentationAdapter, queueView, attestation) {
|
|
1463
|
+
root.innerHTML = renderReviewQueueSessionHtml(session, presentationAdapter, queueView ?? normalizeReviewQueueView(undefined), attestation ?? attestReviewQueueExtraction(session.items, undefined));
|
|
1387
1464
|
bindFieldCardInteractions(root, controller);
|
|
1388
1465
|
bindApplyButton(root, controller);
|
|
1389
1466
|
bindClampToggles(root);
|
|
@@ -1431,6 +1508,9 @@ function queueSessionFromStartState(startState) {
|
|
|
1431
1508
|
...(startState.attemptEvidenceIds?.length
|
|
1432
1509
|
? { attemptEvidenceIdsByItemName: { [startState.item.metadata.name]: [...startState.attemptEvidenceIds] } }
|
|
1433
1510
|
: {}),
|
|
1511
|
+
...(startState.decision === "select-proposed" && startState.selectedCandidateId !== undefined
|
|
1512
|
+
? { selectedCandidateIdsByItemName: { [startState.item.metadata.name]: startState.selectedCandidateId } }
|
|
1513
|
+
: {}),
|
|
1434
1514
|
reviewedAt: startState.reviewedAt,
|
|
1435
1515
|
actorId: startState.actorId,
|
|
1436
1516
|
};
|
|
@@ -1488,6 +1568,21 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1488
1568
|
controller.renderCurrentState();
|
|
1489
1569
|
});
|
|
1490
1570
|
});
|
|
1571
|
+
// One value of a conflict: the button names its candidate, and the decision
|
|
1572
|
+
// records that id. The builder refuses an id that is not one of the item's
|
|
1573
|
+
// proposed candidates, so a stale or forged button changes nothing.
|
|
1574
|
+
root.querySelectorAll("[data-testid='select-value']").forEach((button) => {
|
|
1575
|
+
button.addEventListener("click", () => {
|
|
1576
|
+
const itemName = button.dataset.itemName ?? "";
|
|
1577
|
+
const candidateId = button.dataset.candidateId ?? "";
|
|
1578
|
+
const item = controller.currentSession().items.find((entry) => entry.metadata.name === itemName);
|
|
1579
|
+
if (!item || conflictSelectionIssue(item, candidateId)) {
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
controller.setDecision(itemName, "select-proposed", undefined, candidateId);
|
|
1583
|
+
controller.renderCurrentState();
|
|
1584
|
+
});
|
|
1585
|
+
});
|
|
1491
1586
|
root.querySelectorAll("[data-testid='keep-current']").forEach((button) => {
|
|
1492
1587
|
button.addEventListener("click", () => {
|
|
1493
1588
|
const itemName = button.dataset.itemName ?? "";
|
|
@@ -1598,7 +1693,7 @@ function refreshAuditPayloadForItem(textarea, controller, itemName) {
|
|
|
1598
1693
|
reviewedAt: session.reviewedAt,
|
|
1599
1694
|
actorId: session.actorId,
|
|
1600
1695
|
};
|
|
1601
|
-
payload.textContent = JSON.stringify(buildReviewDecision(state) ?? null, null, 2);
|
|
1696
|
+
payload.textContent = JSON.stringify(buildReviewDecision(state, { presentationAdapter: controller.presentationAdapter }) ?? null, null, 2);
|
|
1602
1697
|
}
|
|
1603
1698
|
function bindClampToggles(root) {
|
|
1604
1699
|
root.querySelectorAll("[data-clamp-toggle]").forEach((button) => {
|
|
@@ -627,6 +627,100 @@ h1, h2, h3, p {
|
|
|
627
627
|
font-size: 12px;
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
/* A queue that came from an extraction import but was not checked against
|
|
631
|
+
it (unverified), or that does not match it (diverges, shown instead of the
|
|
632
|
+
queue). */
|
|
633
|
+
.queue-attestation {
|
|
634
|
+
display: flex;
|
|
635
|
+
align-items: flex-start;
|
|
636
|
+
gap: var(--k-space-2);
|
|
637
|
+
margin: 0 0 var(--k-space-3);
|
|
638
|
+
padding: var(--k-space-2) var(--k-space-3);
|
|
639
|
+
border-radius: 9px;
|
|
640
|
+
font-size: 13px;
|
|
641
|
+
line-height: 1.45;
|
|
642
|
+
color: var(--k-caution);
|
|
643
|
+
background: var(--k-caution-wash);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.queue-attestation svg {
|
|
647
|
+
flex: none;
|
|
648
|
+
margin-top: 2px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.queue-attestation.diverges {
|
|
652
|
+
color: var(--k-negative);
|
|
653
|
+
background: var(--k-negative-wash);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* Conflicting proposed values: each value is listed with its own choice
|
|
657
|
+
control until the field is decided, then marked chosen or not chosen. */
|
|
658
|
+
.conflict-values ul {
|
|
659
|
+
list-style: none;
|
|
660
|
+
margin: var(--k-space-2) 0 0;
|
|
661
|
+
padding: 0;
|
|
662
|
+
display: grid;
|
|
663
|
+
gap: var(--k-space-2);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.conflict-values li {
|
|
667
|
+
display: flex;
|
|
668
|
+
align-items: center;
|
|
669
|
+
justify-content: space-between;
|
|
670
|
+
gap: var(--k-space-3);
|
|
671
|
+
flex-wrap: wrap;
|
|
672
|
+
padding: var(--k-space-2) var(--k-space-3);
|
|
673
|
+
border: 1px solid var(--k-line);
|
|
674
|
+
border-radius: 9px;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.conflict-values li[data-chosen="true"] {
|
|
678
|
+
border-color: var(--k-positive);
|
|
679
|
+
background: var(--k-positive-wash);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
.conflict-values .conflict-text {
|
|
683
|
+
display: inline-flex;
|
|
684
|
+
align-items: baseline;
|
|
685
|
+
gap: var(--k-space-2);
|
|
686
|
+
flex-wrap: wrap;
|
|
687
|
+
min-width: 0;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
.conflict-values .vtext {
|
|
691
|
+
font-weight: 600;
|
|
692
|
+
overflow-wrap: anywhere;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.conflict-values q {
|
|
696
|
+
color: var(--k-muted);
|
|
697
|
+
font-size: 12.5px;
|
|
698
|
+
overflow-wrap: anywhere;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.conflict-values .btn.choose {
|
|
702
|
+
min-height: 36px;
|
|
703
|
+
padding: 0 14px;
|
|
704
|
+
font-size: 13px;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.choice-tag {
|
|
708
|
+
font-size: 12px;
|
|
709
|
+
font-weight: 600;
|
|
710
|
+
padding: 2px 8px;
|
|
711
|
+
border-radius: 999px;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
.choice-tag.chosen {
|
|
715
|
+
color: var(--k-positive);
|
|
716
|
+
background: var(--k-positive-wash);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.choice-tag.not-chosen {
|
|
720
|
+
color: var(--k-muted);
|
|
721
|
+
background: var(--k-sunken);
|
|
722
|
+
}
|
|
723
|
+
|
|
630
724
|
.excerpt-verification,
|
|
631
725
|
.support-verification {
|
|
632
726
|
margin: 0;
|