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