@kontourai/survey 6.1.0 → 8.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/calibration.d.ts +15 -0
- package/dist/src/calibration.js +11 -0
- package/dist/src/canonical-reviewed-trust-input.d.ts +10 -0
- package/dist/src/canonical-reviewed-trust-input.js +45 -1
- package/dist/src/console/review-console-server.js +22 -4
- package/dist/src/decision-carry-forward.d.ts +157 -0
- package/dist/src/decision-carry-forward.js +295 -0
- package/dist/src/extraction-envelope.d.ts +4 -3
- package/dist/src/extraction-envelope.js +3 -3
- package/dist/src/field-states.d.ts +100 -0
- package/dist/src/field-states.js +169 -0
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +3 -1
- package/dist/src/mcp/review-mcp.js +173 -45
- package/dist/src/review-resource.d.ts +36 -0
- package/dist/src/review-session-file.d.ts +7 -0
- package/dist/src/review-workbench/audit-rows.d.ts +1 -1
- package/dist/src/review-workbench/audit-rows.js +1 -0
- package/dist/src/review-workbench/audit-sample.d.ts +32 -0
- package/dist/src/review-workbench/audit-sample.js +53 -0
- package/dist/src/review-workbench/extraction-inspector.d.ts +1 -1
- package/dist/src/review-workbench/extraction-inspector.js +1 -1
- package/dist/src/review-workbench/field-state-view.d.ts +15 -0
- package/dist/src/review-workbench/field-state-view.js +84 -0
- package/dist/src/review-workbench/queue-binding.d.ts +45 -2
- package/dist/src/review-workbench/queue-binding.js +45 -0
- package/dist/src/review-workbench/review-presentation.d.ts +23 -0
- package/dist/src/review-workbench/review-presentation.js +51 -10
- package/dist/src/review-workbench/review-queue-session.d.ts +93 -5
- package/dist/src/review-workbench/review-queue-session.js +147 -11
- package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
- package/dist/src/review-workbench/review-session-replay.js +49 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +126 -0
- package/dist/src/review-workbench/review-workbench-element.d.ts +10 -0
- package/dist/src/review-workbench/review-workbench-element.js +21 -3
- package/dist/src/review-workbench/review-workbench.css +126 -0
- package/dist/src/review-workbench/review-workbench.d.ts +40 -8
- package/dist/src/review-workbench/review-workbench.js +192 -71
- package/dist/src/review-workbench/review-workbench.standalone.css +126 -0
- package/dist/src/review-workbench/server-review-session.d.ts +18 -0
- package/dist/src/review-workbench/server-review-session.js +20 -2
- package/dist/src/surface-reviewed-extraction.d.ts +23 -0
- package/dist/src/surface-reviewed-extraction.js +64 -1
- package/dist/src/to-surface.js +32 -3
- package/package.json +1 -1
package/dist/src/index.js
CHANGED
|
@@ -2,7 +2,7 @@ export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
|
2
2
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
3
3
|
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
4
4
|
export { buildExtractionInspectorModel, exportExtractionInspector, inspectorSourcePosture, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
5
|
-
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./review-workbench/queue-binding.js";
|
|
5
|
+
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, attestReviewQueueExtraction, bindReviewQueue, reviewItemCarriesExtractionBinding, unverifiedExtractionQueueSentence, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./review-workbench/queue-binding.js";
|
|
6
6
|
export { resolvePortablePdfRegion } from "./pdf-layout.js";
|
|
7
7
|
export { toSurfaceReviewedExtractionDecision, toSurfaceReviewedExtractionImport, toSurfaceReviewedExtractionItem, } from "./surface-reviewed-extraction.js";
|
|
8
8
|
export { candidateReviewRecord, candidateSetStatusFor, SurveyInputBuilder } from "./builder.js";
|
|
@@ -28,3 +28,5 @@ export { confidenceBasisForReview, defineProductVocabulary, stableId } from "./v
|
|
|
28
28
|
export { currentProposedReviewItem } from "./current-proposed-review-item.js";
|
|
29
29
|
export { deriveOversightMetrics, mergeTrustBundleWithOversightMetrics, oversightMetricsToClaims, } from "./oversight-metrics.js";
|
|
30
30
|
export { calibrationToClaims, deriveCalibration, mergeTrustBundleWithCalibration, } from "./calibration.js";
|
|
31
|
+
export { deriveFieldStates, fieldStateClaimMetadata, lifecycleForDecision, FIELD_STATE_METADATA_KEY, } from "./field-states.js";
|
|
32
|
+
export { buildDecisionSupersession, decisionCarryForwardKind, decisionReference, decisionResolution, decisionSupersessionKind, defaultCarryForwardPolicy, splitRoundForCarryForward, validateDecisionCarryForward, validateDecisionSupersession, } from "./decision-carry-forward.js";
|
|
@@ -3,9 +3,9 @@ import { resolve, dirname } from "node:path";
|
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
4
4
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { buildReviewSessionEvents, currentReviewItem, decisionSelectsNoCandidate, deriveQueueRowStatus, nextUnresolvedItemName, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
|
|
6
|
+
import { attestReviewQueueExtraction, buildReviewSessionEvents, conflictSelectionIssue, currentReviewItem, decisionSelectsNoCandidate, deriveQueueRowStatus, isScoreBlind, nextUnresolvedItemName, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
|
|
7
7
|
import { createServerReviewSessionRecord, currentSessionState, deriveServerReviewSessionApplyResult, } from "../review-workbench/server-review-session.js";
|
|
8
|
-
import { buildReviewItemPresentation, candidateVerificationNotes, excludedProposalsSentence } from "../review-workbench/review-presentation.js";
|
|
8
|
+
import { buildReviewItemPresentation, candidateVerificationNotes, excludedProposalsSentence, excludedProposalsUnreadableSentence } from "../review-workbench/review-presentation.js";
|
|
9
9
|
import { appendReviewSessionEvents, readReviewSessionFile, storedReviewSessionName, updateReviewSessionFile, } from "../review-session-file.js";
|
|
10
10
|
const SESSION_NAME = "mcp-review-session";
|
|
11
11
|
const UI_RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
@@ -16,15 +16,41 @@ const SERVER_INSTRUCTIONS = "Use survey_review_queue to inspect the queue, surve
|
|
|
16
16
|
// MCP tool decision strings → ReviewWorkbenchDecision
|
|
17
17
|
const MCP_DECISION_MAP = {
|
|
18
18
|
accept: "accept-proposed",
|
|
19
|
+
select: "select-proposed",
|
|
19
20
|
hold: "keep-current",
|
|
20
21
|
reject: "reject-proposed",
|
|
21
22
|
"could-not-confirm": "could-not-confirm",
|
|
22
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Reads the session and checks its queue against the extraction import stored
|
|
26
|
+
* beside it. A queue that does not match its import is refused outright; one
|
|
27
|
+
* whose items came from an import but has no record stored is presented with
|
|
28
|
+
* an "Unverified queue" notice on every surface.
|
|
29
|
+
*/
|
|
23
30
|
async function readSessionFile(path) {
|
|
24
|
-
|
|
31
|
+
const file = await readReviewSessionFile(path);
|
|
32
|
+
return { ...file, attestation: attestedQueue(file) };
|
|
33
|
+
}
|
|
34
|
+
function attestedQueue(file) {
|
|
35
|
+
const attestation = attestReviewQueueExtraction(file.snapshot.items, file.extractionImport);
|
|
36
|
+
if (attestation.state === "diverges") {
|
|
37
|
+
throw new DomainError(attestation.message);
|
|
38
|
+
}
|
|
39
|
+
return attestation;
|
|
40
|
+
}
|
|
41
|
+
function attestationLines(attestation) {
|
|
42
|
+
return attestation.state === "unverified" ? [attestation.message, ``] : [];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A score-blind session shows no confidence and no verifier result on any
|
|
46
|
+
* surface (text, data or card), so the decision is made without them.
|
|
47
|
+
*/
|
|
48
|
+
const SCORE_BLIND_NOTICE = "Score-blind review: confidence and verifier results are hidden for this whole session. Decide from the source and its excerpt.";
|
|
49
|
+
function scoreBlindLines(state) {
|
|
50
|
+
return isScoreBlind(state) ? [`${state.sampling?.kind === "random-audit" ? "Random audit sample. " : ""}${SCORE_BLIND_NOTICE}`, ``] : [];
|
|
25
51
|
}
|
|
26
52
|
// ---- Queue helpers -------------------------------------------------------
|
|
27
|
-
function queueSummaryText(snapshot, events) {
|
|
53
|
+
function queueSummaryText(snapshot, events, attestation) {
|
|
28
54
|
const current = currentSessionState(snapshot, events);
|
|
29
55
|
const summary = reviewSessionSummary(current);
|
|
30
56
|
const total = current.items.length;
|
|
@@ -38,6 +64,8 @@ function queueSummaryText(snapshot, events) {
|
|
|
38
64
|
return ` ${item.metadata.name} — ${item.spec.target} — ${status}${marker}`;
|
|
39
65
|
});
|
|
40
66
|
return [
|
|
67
|
+
...attestationLines(attestation),
|
|
68
|
+
...scoreBlindLines(current),
|
|
41
69
|
`Review queue: ${resolved}/${total} resolved`,
|
|
42
70
|
`Active item: ${activeItem.metadata.name} (${activeItem.spec.target})`,
|
|
43
71
|
...(nextItem ? [`Next unresolved: ${nextItem}`] : ["All items resolved."]),
|
|
@@ -48,16 +76,21 @@ function queueSummaryText(snapshot, events) {
|
|
|
48
76
|
...rows,
|
|
49
77
|
].join("\n");
|
|
50
78
|
}
|
|
51
|
-
function itemDetailText(item, snapshot, events) {
|
|
79
|
+
function itemDetailText(item, snapshot, events, attestation) {
|
|
52
80
|
const current = currentSessionState(snapshot, events);
|
|
53
81
|
const status = deriveQueueRowStatus(item, current);
|
|
54
82
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
55
83
|
const note = current.notesByItemName[item.metadata.name];
|
|
84
|
+
const chosenId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
56
85
|
const currentCandidate = item.spec.candidates.find((c) => c.role === "current");
|
|
57
86
|
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
58
87
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
59
88
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "unknown";
|
|
89
|
+
const blind = isScoreBlind(current);
|
|
90
|
+
const confidenceLine = (c) => blind ? [] : [` confidence: ${confStr(c)}`];
|
|
60
91
|
const lines = [
|
|
92
|
+
...attestationLines(attestation),
|
|
93
|
+
...scoreBlindLines(current),
|
|
61
94
|
`Item: ${item.metadata.name}`,
|
|
62
95
|
`Target: ${item.spec.target}`,
|
|
63
96
|
`Status: ${status}`,
|
|
@@ -66,21 +99,25 @@ function itemDetailText(item, snapshot, events) {
|
|
|
66
99
|
...(note ? [`Note: ${note}`] : []),
|
|
67
100
|
``,
|
|
68
101
|
`Current value: ${valueStr(currentCandidate?.value ?? "(none)")}`,
|
|
69
|
-
|
|
102
|
+
...confidenceLine(currentCandidate?.extraction?.confidence ?? currentCandidate?.confidence),
|
|
70
103
|
` source: ${currentCandidate?.source?.sourceRef ?? "none"}`,
|
|
71
104
|
...(currentCandidate?.locator?.excerpt ? [` excerpt: ${currentCandidate.locator.excerpt}`] : []),
|
|
72
105
|
``,
|
|
73
106
|
...(proposedCandidates.length > 1
|
|
74
|
-
? [`Conflict: ${proposedCandidates.length} proposed values. Accept is refused; reject them all or use could-not-confirm with a reason.`]
|
|
107
|
+
? [`Conflict: ${proposedCandidates.length} proposed values. Accept is refused; choose one with decision "select" and its candidateId, reject them all, or use could-not-confirm with a reason.`]
|
|
108
|
+
: []),
|
|
109
|
+
...(chosenId !== undefined
|
|
110
|
+
? [`Chosen value: ${valueStr(proposedCandidates.find((c) => c.id === chosenId)?.value)} (candidate ${chosenId}); not chosen: ${proposedCandidates.filter((c) => c.id !== chosenId).map((c) => `${valueStr(c.value)} (candidate ${c.id})`).join(", ")}`]
|
|
75
111
|
: []),
|
|
76
112
|
...(proposedCandidates.length === 0 ? [`Proposed value: (none)`] : proposedCandidates.flatMap((candidate) => [
|
|
77
|
-
`Proposed value: ${valueStr(candidate.value)}`,
|
|
78
|
-
`
|
|
113
|
+
`Proposed value: ${valueStr(candidate.value)}${chosenId === undefined ? "" : candidate.id === chosenId ? " [chosen]" : " [not chosen]"}`,
|
|
114
|
+
...(proposedCandidates.length > 1 ? [` candidateId: ${candidate.id}`] : []),
|
|
115
|
+
...confidenceLine(candidate.extraction?.confidence ?? candidate.confidence),
|
|
79
116
|
` source: ${candidate.source?.sourceRef ?? "none"}`,
|
|
80
117
|
...(candidate.locator?.excerpt ? [` excerpt: ${candidate.locator.excerpt}`] : []),
|
|
81
118
|
])),
|
|
82
119
|
...extractionImportLines(item),
|
|
83
|
-
...candidateVerificationNotes(item, editedValueFor(item, current)).flatMap((entry) => [``, `Verification: ${entry.sentence}`]),
|
|
120
|
+
...(blind ? [] : candidateVerificationNotes(item, editedValueFor(item, current)).flatMap((entry) => [``, `Verification: ${entry.sentence}`])),
|
|
84
121
|
];
|
|
85
122
|
if (item.spec.rationale) {
|
|
86
123
|
lines.push(``, `Rationale: ${item.spec.rationale}`);
|
|
@@ -88,6 +125,13 @@ function itemDetailText(item, snapshot, events) {
|
|
|
88
125
|
return lines.join("\n");
|
|
89
126
|
}
|
|
90
127
|
/** The reviewer's edit when the item's decision accepts one; verifier records are read against it. */
|
|
128
|
+
/** The session's presentation and sampling, as a client should read them. */
|
|
129
|
+
function sessionConditions(snapshot) {
|
|
130
|
+
return {
|
|
131
|
+
...(snapshot.presentation !== undefined ? { presentation: snapshot.presentation } : {}),
|
|
132
|
+
...(snapshot.sampling !== undefined ? { sampling: snapshot.sampling } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
91
135
|
function editedValueFor(item, state) {
|
|
92
136
|
return state.decisionsByItemName[item.metadata.name] === "accept-proposed" ? state.editedValuesByItemName?.[item.metadata.name] : undefined;
|
|
93
137
|
}
|
|
@@ -95,8 +139,10 @@ function editedValueFor(item, state) {
|
|
|
95
139
|
function extractionImportLines(item) {
|
|
96
140
|
const presentation = buildReviewItemPresentation(item);
|
|
97
141
|
const excluded = excludedProposalsSentence(presentation.excludedProposals);
|
|
142
|
+
const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
|
|
98
143
|
return [
|
|
99
144
|
...(excluded ? [``, `Excluded: ${excluded} Check the source before accepting.`] : []),
|
|
145
|
+
...(unreadable ? [``, `Excluded: ${unreadable} Check the source before accepting.`] : []),
|
|
100
146
|
...(presentation.excerptVerification ? [``, `Excerpts ${presentation.excerptVerification === "verified" ? "were" : "were not"} checked against the prepared source text at import.`] : []),
|
|
101
147
|
];
|
|
102
148
|
}
|
|
@@ -114,43 +160,57 @@ function escapeHtml(text) {
|
|
|
114
160
|
.replace(/>/g, ">")
|
|
115
161
|
.replace(/"/g, """);
|
|
116
162
|
}
|
|
117
|
-
function buildReviewCardHtml(item, snapshot, events) {
|
|
163
|
+
function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
118
164
|
const current = currentSessionState(snapshot, events);
|
|
119
165
|
const summary = reviewSessionSummary(current);
|
|
120
166
|
const total = current.items.length;
|
|
121
167
|
const resolved = total - summary.unresolved;
|
|
122
168
|
const currentCandidate = item.spec.candidates.find((c) => c.role === "current");
|
|
123
169
|
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
124
|
-
// Several proposed values are a conflict: every value is shown,
|
|
125
|
-
// (which names a role, not a value) is not offered
|
|
170
|
+
// Several proposed values are a conflict: every value is shown, accept
|
|
171
|
+
// (which names a role, not a value) is not offered, and each value has its
|
|
172
|
+
// own "Use this value" control that names its candidate id.
|
|
126
173
|
const conflict = proposedCandidates.length > 1;
|
|
127
174
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
175
|
+
const chosenId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
128
176
|
const status = deriveQueueRowStatus(item, current);
|
|
129
177
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v, null, 2);
|
|
130
178
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "—";
|
|
179
|
+
const blind = isScoreBlind(current);
|
|
180
|
+
const confHtml = (c) => blind ? "" : `<div class="conf">confidence ${confStr(c)}</div>`;
|
|
131
181
|
const currentValue = valueStr(currentCandidate?.value ?? "—");
|
|
132
|
-
const currentConf =
|
|
182
|
+
const currentConf = confHtml(currentCandidate?.extraction?.confidence ?? currentCandidate?.confidence);
|
|
133
183
|
const currentSource = currentCandidate?.source?.sourceRef ?? "—";
|
|
134
184
|
const currentExcerpt = currentCandidate?.locator?.excerpt ?? "";
|
|
135
185
|
const proposedCard = (candidate, label) => {
|
|
136
186
|
const value = valueStr(candidate?.value ?? "—");
|
|
137
187
|
const excerpt = candidate?.locator?.excerpt ?? "";
|
|
138
|
-
|
|
188
|
+
const choice = conflict && candidate
|
|
189
|
+
? chosenId !== undefined
|
|
190
|
+
? `<div class="choice ${candidate.id === chosenId ? "chosen" : "not-chosen"}" data-candidate-id="${escapeHtml(candidate.id)}">${candidate.id === chosenId ? "Chosen" : "Not chosen"}</div>`
|
|
191
|
+
: decision === undefined
|
|
192
|
+
? `<button class="btn btn-accept btn-select" data-candidate-id="${escapeHtml(candidate.id)}">Use this value</button>`
|
|
193
|
+
: ""
|
|
194
|
+
: "";
|
|
195
|
+
return `<div class="card is-proposed"${conflict && candidate ? ` data-candidate-id="${escapeHtml(candidate.id)}"` : ""}>
|
|
139
196
|
<div class="card-label">${escapeHtml(label)}</div>
|
|
140
197
|
<div class="value">${value.includes("\n") ? `<pre>${escapeHtml(value)}</pre>` : escapeHtml(value)}</div>
|
|
141
|
-
|
|
198
|
+
${confHtml(candidate?.extraction?.confidence ?? candidate?.confidence)}
|
|
142
199
|
<div class="source-ref">${escapeHtml(candidate?.source?.sourceRef ?? "—")}</div>
|
|
143
200
|
${excerpt ? `<div class="excerpt">${escapeHtml(excerpt)}</div>` : ""}
|
|
201
|
+
${choice}
|
|
144
202
|
</div>`;
|
|
145
203
|
};
|
|
146
204
|
const proposedCards = conflict
|
|
147
205
|
? proposedCandidates.map((candidate, index) => proposedCard(candidate, `Proposed ${index + 1} of ${proposedCandidates.length}`)).join("\n ")
|
|
148
206
|
: proposedCard(proposedCandidates[0], "Proposed");
|
|
149
207
|
const itemNameJson = escapeJsonInHtml(item.metadata.name);
|
|
150
|
-
const
|
|
151
|
-
const
|
|
208
|
+
const itemPresentation = buildReviewItemPresentation(item);
|
|
209
|
+
const excludedNote = excludedProposalsSentence(itemPresentation.excludedProposals);
|
|
210
|
+
const unreadableNote = excludedProposalsUnreadableSentence(itemPresentation.excludedProposalsUnreadable);
|
|
211
|
+
const verificationNotes = blind ? [] : candidateVerificationNotes(item, editedValueFor(item, current));
|
|
152
212
|
const decisionBadge = decision
|
|
153
|
-
? `<span class="badge badge-${decision === "accept-proposed" ? "accept" : decision === "reject-proposed" ? "reject" : "hold"}">${escapeHtml(workbenchDecisionDefinitions[decision].label)}</span>`
|
|
213
|
+
? `<span class="badge badge-${decision === "accept-proposed" || decision === "select-proposed" ? "accept" : decision === "reject-proposed" ? "reject" : "hold"}">${escapeHtml(decision === "select-proposed" ? `Chose 1 of ${proposedCandidates.length} values` : workbenchDecisionDefinitions[decision].label)}</span>`
|
|
154
214
|
: `<span class="badge badge-pending">${escapeHtml(status)}</span>`;
|
|
155
215
|
return `<!doctype html>
|
|
156
216
|
<html lang="en">
|
|
@@ -228,9 +288,16 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
228
288
|
.btn-reject:hover,.btn-reject.active{background:color-mix(in srgb,var(--k-negative) 16%,transparent);border-color:var(--k-negative);color:var(--k-negative)}
|
|
229
289
|
.btn-unconfirmed:hover,.btn-unconfirmed.active{background:color-mix(in srgb,var(--k-caution) 16%,transparent);border-color:var(--k-caution);color:var(--k-caution)}
|
|
230
290
|
.feedback{font-size:11px;color:var(--k-text-faint);margin-top:8px;min-height:16px}
|
|
291
|
+
.btn-select{width:100%;margin-top:8px}
|
|
292
|
+
.choice{font-family:var(--k-font-mono);font-size:10px;font-weight:600;margin-top:8px;text-transform:uppercase;letter-spacing:.06em}
|
|
293
|
+
.choice.chosen{color:var(--k-positive)}
|
|
294
|
+
.choice.not-chosen{color:var(--k-text-faint)}
|
|
295
|
+
.notice{font-size:11px;color:var(--k-caution);background:color-mix(in srgb,var(--k-caution) 12%,transparent);border-radius:var(--k-radius-sm);padding:7px 10px;margin:0 0 12px}
|
|
231
296
|
</style>
|
|
232
297
|
</head>
|
|
233
298
|
<body>
|
|
299
|
+
${attestation.state === "unverified" ? `<p class="notice" id="unverified-queue-note">${escapeHtml(attestation.message)}</p>` : ""}
|
|
300
|
+
${blind ? `<p class="notice" id="score-blind-note">${escapeHtml(scoreBlindLines(current)[0])}</p>` : ""}
|
|
234
301
|
<p class="eyebrow">Survey Review</p>
|
|
235
302
|
<h1>${escapeHtml(item.spec.target)}</h1>
|
|
236
303
|
<div class="meta">
|
|
@@ -244,15 +311,18 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
244
311
|
<div class="card">
|
|
245
312
|
<div class="card-label">Current</div>
|
|
246
313
|
<div class="value">${currentValue.includes("\n") ? `<pre>${escapeHtml(currentValue)}</pre>` : escapeHtml(currentValue)}</div>
|
|
247
|
-
|
|
314
|
+
${currentConf}
|
|
248
315
|
<div class="source-ref">${escapeHtml(currentSource)}</div>
|
|
249
316
|
${currentExcerpt ? `<div class="excerpt">${escapeHtml(currentExcerpt)}</div>` : ""}
|
|
250
317
|
</div>
|
|
251
318
|
${proposedCards}
|
|
252
319
|
</div>
|
|
253
320
|
${excludedNote ? `<p class="feedback" id="excluded-note">${escapeHtml(excludedNote)}</p>` : ""}
|
|
321
|
+
${unreadableNote ? `<p class="feedback" id="excluded-unreadable-note">${escapeHtml(unreadableNote)}</p>` : ""}
|
|
254
322
|
${verificationNotes.map((entry) => `<p class="feedback verification-note" data-candidate-id="${escapeHtml(entry.candidateId)}">${escapeHtml(entry.sentence)}</p>`).join("\n")}
|
|
255
|
-
${conflict ? `<p class="feedback" id="conflict-note">${
|
|
323
|
+
${conflict ? `<p class="feedback" id="conflict-note">${escapeHtml(chosenId !== undefined
|
|
324
|
+
? `${proposedCandidates.length} different values were proposed. One was chosen; ${proposedCandidates.length === 2 ? "the other was" : "the others were"} seen and not chosen.`
|
|
325
|
+
: `${proposedCandidates.length} different values were proposed. Use the value the source supports, reject them all, or use Could not confirm with a reason.`)}</p>` : ""}
|
|
256
326
|
|
|
257
327
|
<div class="divider"></div>
|
|
258
328
|
|
|
@@ -272,7 +342,7 @@ ${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length
|
|
|
272
342
|
var itemName = ${itemNameJson};
|
|
273
343
|
var msgId = 1;
|
|
274
344
|
|
|
275
|
-
function postDecision(decision) {
|
|
345
|
+
function postDecision(decision, candidateId) {
|
|
276
346
|
var note = document.getElementById('note').value;
|
|
277
347
|
if (decision === 'could-not-confirm' && !note.trim()) {
|
|
278
348
|
document.getElementById('feedback').textContent = 'A reason is required when you could not confirm.';
|
|
@@ -287,12 +357,18 @@ ${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length
|
|
|
287
357
|
name: "survey_review_decide",
|
|
288
358
|
arguments: decision === 'could-not-confirm'
|
|
289
359
|
? { itemName: itemName, decision: decision, reason: note }
|
|
290
|
-
:
|
|
360
|
+
: decision === 'select'
|
|
361
|
+
? { itemName: itemName, decision: decision, candidateId: candidateId, note: note || undefined }
|
|
362
|
+
: { itemName: itemName, decision: decision, note: note || undefined }
|
|
291
363
|
}
|
|
292
364
|
}, "*");
|
|
293
365
|
return true;
|
|
294
366
|
}
|
|
295
367
|
|
|
368
|
+
Array.prototype.forEach.call(document.querySelectorAll('.btn-select'), function (button) {
|
|
369
|
+
button.addEventListener('click', function () { postDecision('select', button.getAttribute('data-candidate-id')); document.getElementById('feedback').textContent = 'Submitting choice…'; });
|
|
370
|
+
});
|
|
371
|
+
|
|
296
372
|
var acceptButton = document.getElementById('btn-accept');
|
|
297
373
|
if (acceptButton) acceptButton.addEventListener('click', function () { postDecision('accept'); document.getElementById('feedback').textContent = 'Submitting accept…'; });
|
|
298
374
|
var holdButton = document.getElementById('btn-hold');
|
|
@@ -318,16 +394,22 @@ ${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length
|
|
|
318
394
|
// ---- Tool implementations ------------------------------------------------
|
|
319
395
|
async function toolQueue(options) {
|
|
320
396
|
const file = await readSessionFile(options.sessionPath);
|
|
321
|
-
const { snapshot, events } = file;
|
|
322
|
-
const text = queueSummaryText(snapshot, events);
|
|
397
|
+
const { snapshot, events, attestation } = file;
|
|
398
|
+
const text = queueSummaryText(snapshot, events, attestation);
|
|
323
399
|
const queueData = {
|
|
400
|
+
queueAttestation: attestation.state,
|
|
401
|
+
...sessionConditions(snapshot),
|
|
324
402
|
items: snapshot.items.map((item) => {
|
|
325
403
|
const current = currentSessionState(snapshot, events);
|
|
404
|
+
const selectedCandidateId = current.decisionsByItemName[item.metadata.name] === "select-proposed"
|
|
405
|
+
? current.selectedCandidateIdsByItemName?.[item.metadata.name]
|
|
406
|
+
: undefined;
|
|
326
407
|
return {
|
|
327
408
|
name: item.metadata.name,
|
|
328
409
|
target: item.spec.target,
|
|
329
410
|
status: deriveQueueRowStatus(item, current),
|
|
330
411
|
decision: current.decisionsByItemName[item.metadata.name],
|
|
412
|
+
...(selectedCandidateId !== undefined ? { selectedCandidateId } : {}),
|
|
331
413
|
candidateSetStatus: item.spec.candidateSetStatus,
|
|
332
414
|
};
|
|
333
415
|
}),
|
|
@@ -339,24 +421,30 @@ async function toolQueue(options) {
|
|
|
339
421
|
];
|
|
340
422
|
if (!options.noUi) {
|
|
341
423
|
const activeItem = currentReviewItem(currentSessionState(snapshot, events));
|
|
342
|
-
content.push(buildUiResource(activeItem, snapshot, events, "queue"));
|
|
424
|
+
content.push(buildUiResource(activeItem, snapshot, events, "queue", attestation));
|
|
343
425
|
}
|
|
344
426
|
return content;
|
|
345
427
|
}
|
|
346
428
|
async function toolItem(itemName, options) {
|
|
347
429
|
const file = await readSessionFile(options.sessionPath);
|
|
348
|
-
const { snapshot, events } = file;
|
|
430
|
+
const { snapshot, events, attestation } = file;
|
|
349
431
|
const current = currentSessionState(snapshot, events);
|
|
350
432
|
const item = current.items.find((i) => i.metadata.name === itemName);
|
|
351
433
|
if (!item) {
|
|
352
434
|
throw new DomainError(`Unknown review item: ${itemName}`);
|
|
353
435
|
}
|
|
354
|
-
const text = itemDetailText(item, snapshot, events);
|
|
436
|
+
const text = itemDetailText(item, snapshot, events, attestation);
|
|
437
|
+
const decision = current.decisionsByItemName[item.metadata.name];
|
|
438
|
+
const selectedCandidateId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
439
|
+
const blind = isScoreBlind(current);
|
|
355
440
|
const itemData = {
|
|
441
|
+
queueAttestation: attestation.state,
|
|
442
|
+
...sessionConditions(snapshot),
|
|
356
443
|
name: item.metadata.name,
|
|
357
444
|
target: item.spec.target,
|
|
358
445
|
status: deriveQueueRowStatus(item, current),
|
|
359
|
-
decision
|
|
446
|
+
decision,
|
|
447
|
+
...(selectedCandidateId !== undefined ? { selectedCandidateId } : {}),
|
|
360
448
|
note: current.notesByItemName[item.metadata.name],
|
|
361
449
|
candidateSetStatus: item.spec.candidateSetStatus,
|
|
362
450
|
...(() => {
|
|
@@ -364,19 +452,21 @@ async function toolItem(itemName, options) {
|
|
|
364
452
|
return {
|
|
365
453
|
...(presentation.excerptVerification ? { excerptVerification: presentation.excerptVerification } : {}),
|
|
366
454
|
...(presentation.excludedProposals.length ? { excludedProposals: presentation.excludedProposals.map(({ proposalIndex, value, locator, excerpt }) => ({ proposalIndex, value, locator, excerpt })) } : {}),
|
|
455
|
+
...(presentation.excludedProposalsUnreadable ? { excludedProposalsUnreadable: presentation.excludedProposalsUnreadable } : {}),
|
|
367
456
|
};
|
|
368
457
|
})(),
|
|
369
458
|
candidates: (() => {
|
|
370
|
-
const notes = candidateVerificationNotes(item, editedValueFor(item, current));
|
|
459
|
+
const notes = blind ? [] : candidateVerificationNotes(item, editedValueFor(item, current));
|
|
371
460
|
return item.spec.candidates.map((c, index) => {
|
|
372
461
|
const note = notes.find((entry) => entry.candidateIndex === index);
|
|
373
462
|
return {
|
|
374
463
|
id: c.id,
|
|
375
464
|
role: c.role,
|
|
376
465
|
value: c.value,
|
|
377
|
-
confidence: c.extraction?.confidence ?? c.confidence,
|
|
466
|
+
...(blind ? {} : { confidence: c.extraction?.confidence ?? c.confidence }),
|
|
378
467
|
sourceRef: c.source?.sourceRef,
|
|
379
468
|
excerpt: c.locator?.excerpt,
|
|
469
|
+
...(selectedCandidateId !== undefined && c.role === "proposed" ? { chosen: c.id === selectedCandidateId } : {}),
|
|
380
470
|
...(note ? { verification: { subject: note.subjectLabel, status: note.status, records: note.records, inapplicableCount: note.inapplicableCount, rejectedCount: note.rejectedCount } } : {}),
|
|
381
471
|
};
|
|
382
472
|
});
|
|
@@ -386,22 +476,23 @@ async function toolItem(itemName, options) {
|
|
|
386
476
|
{ type: "text", text: `${text}\n\n${JSON.stringify(itemData, null, 2)}` },
|
|
387
477
|
];
|
|
388
478
|
if (!options.noUi) {
|
|
389
|
-
content.push(buildUiResource(item, snapshot, events, itemName));
|
|
479
|
+
content.push(buildUiResource(item, snapshot, events, itemName, attestation));
|
|
390
480
|
}
|
|
391
481
|
return content;
|
|
392
482
|
}
|
|
393
|
-
async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, options) {
|
|
483
|
+
async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, options, selectedCandidateId) {
|
|
394
484
|
const wbDecision = MCP_DECISION_MAP[mcpDecision];
|
|
395
485
|
if (!wbDecision) {
|
|
396
|
-
throw new DomainError(`Invalid decision: ${mcpDecision}. Must be accept, hold, reject, or could-not-confirm.`);
|
|
486
|
+
throw new DomainError(`Invalid decision: ${mcpDecision}. Must be accept, select, hold, reject, or could-not-confirm.`);
|
|
397
487
|
}
|
|
398
488
|
if (wbDecision === "could-not-confirm" && !note?.trim()) {
|
|
399
489
|
throw new DomainError("survey_review_decide requires a non-empty reason for could-not-confirm");
|
|
400
490
|
}
|
|
401
491
|
// Read, validate and write inside the shared session lock so a concurrent
|
|
402
492
|
// decide or console save cannot interleave and drop this decision (#281).
|
|
403
|
-
const { snapshot, sessionWithDecision, newEvents } = await updateReviewSessionFile(options.sessionPath, (file) => {
|
|
493
|
+
const { snapshot, sessionWithDecision, newEvents, attestation } = await updateReviewSessionFile(options.sessionPath, (file) => {
|
|
404
494
|
const { snapshot, events } = file;
|
|
495
|
+
const attestation = attestedQueue(file);
|
|
405
496
|
const current = currentSessionState(snapshot, events);
|
|
406
497
|
const item = current.items.find((i) => i.metadata.name === itemName);
|
|
407
498
|
if (!item) {
|
|
@@ -411,6 +502,12 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
411
502
|
if (existingDecision) {
|
|
412
503
|
throw new DomainError(`Item ${itemName} already has a decision: ${existingDecision}. Use a new session to re-decide.`);
|
|
413
504
|
}
|
|
505
|
+
if (wbDecision === "select-proposed") {
|
|
506
|
+
const selectionIssue = conflictSelectionIssue(item, selectedCandidateId);
|
|
507
|
+
if (selectionIssue) {
|
|
508
|
+
throw new DomainError(selectionIssue);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
414
511
|
// Build the updated session state with the decision
|
|
415
512
|
const sessionWithDecision = {
|
|
416
513
|
...current,
|
|
@@ -434,6 +531,14 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
434
531
|
},
|
|
435
532
|
}
|
|
436
533
|
: {}),
|
|
534
|
+
...(wbDecision === "select-proposed" && selectedCandidateId !== undefined
|
|
535
|
+
? {
|
|
536
|
+
selectedCandidateIdsByItemName: {
|
|
537
|
+
...current.selectedCandidateIdsByItemName,
|
|
538
|
+
[itemName]: selectedCandidateId,
|
|
539
|
+
},
|
|
540
|
+
}
|
|
541
|
+
: {}),
|
|
437
542
|
};
|
|
438
543
|
// Append only this decision's events (its note, then the decision) to the
|
|
439
544
|
// stored log. Regenerating the whole log from state would erase earlier
|
|
@@ -455,26 +560,36 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
455
560
|
record,
|
|
456
561
|
events: newEvents,
|
|
457
562
|
requiredResolvedItems: "none",
|
|
563
|
+
extractionImport: file.extractionImport,
|
|
458
564
|
});
|
|
459
565
|
if (!applyResult.ok) {
|
|
460
566
|
throw new DomainError(`Decision validation failed: ${applyResult.issues.map((issue) => "message" in issue ? issue.message : String(issue)).join("; ")}`);
|
|
461
567
|
}
|
|
462
568
|
const updatedFile = {
|
|
569
|
+
...file,
|
|
463
570
|
session: file.session,
|
|
464
571
|
snapshot,
|
|
465
572
|
events: newEvents,
|
|
466
573
|
};
|
|
467
|
-
return { next: updatedFile, result: { snapshot, sessionWithDecision, newEvents } };
|
|
574
|
+
return { next: updatedFile, result: { snapshot, sessionWithDecision, newEvents, attestation } };
|
|
468
575
|
});
|
|
469
576
|
// Summarize the result
|
|
470
577
|
const updatedItem = sessionWithDecision.items.find((i) => i.metadata.name === itemName);
|
|
471
|
-
const itemText = updatedItem ? itemDetailText(updatedItem, snapshot, newEvents) : `Item: ${itemName}`;
|
|
472
|
-
const remainingText = queueSummaryText(snapshot, newEvents);
|
|
578
|
+
const itemText = updatedItem ? itemDetailText(updatedItem, snapshot, newEvents, attestation) : `Item: ${itemName}`;
|
|
579
|
+
const remainingText = queueSummaryText(snapshot, newEvents, attestation);
|
|
473
580
|
const definition = workbenchDecisionDefinitions[wbDecision];
|
|
474
581
|
const conflictRejected = wbDecision === "reject-proposed" && updatedItem !== undefined && decisionSelectsNoCandidate(updatedItem, wbDecision);
|
|
582
|
+
const valueStr = (v) => (typeof v === "string" ? v : JSON.stringify(v));
|
|
583
|
+
const proposed = updatedItem?.spec.candidates.filter((c) => c.role === "proposed") ?? [];
|
|
584
|
+
const chosen = proposed.find((c) => c.id === selectedCandidateId);
|
|
585
|
+
const effect = conflictRejected
|
|
586
|
+
? "Every proposed value is rejected; none becomes the claim's value."
|
|
587
|
+
: wbDecision === "select-proposed"
|
|
588
|
+
? `${valueStr(chosen?.value)} becomes the verified value; not chosen: ${proposed.filter((c) => c.id !== selectedCandidateId).map((c) => valueStr(c.value)).join(", ")}.`
|
|
589
|
+
: definition.effect;
|
|
475
590
|
const text = [
|
|
476
591
|
`Decision recorded: ${conflictRejected ? "Reject all values" : definition.label}`,
|
|
477
|
-
`Effect: ${
|
|
592
|
+
`Effect: ${effect}`,
|
|
478
593
|
"",
|
|
479
594
|
itemText,
|
|
480
595
|
"",
|
|
@@ -483,13 +598,13 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
483
598
|
].join("\n");
|
|
484
599
|
return [{ type: "text", text }];
|
|
485
600
|
}
|
|
486
|
-
function buildUiResource(item, snapshot, events, instance) {
|
|
601
|
+
function buildUiResource(item, snapshot, events, instance, attestation) {
|
|
487
602
|
return {
|
|
488
603
|
type: "resource",
|
|
489
604
|
resource: {
|
|
490
605
|
uri: `ui://survey/review-card/${encodeURIComponent(instance)}`,
|
|
491
606
|
mimeType: "text/html;profile=mcp-app",
|
|
492
|
-
text: buildReviewCardHtml(item, snapshot, events),
|
|
607
|
+
text: buildReviewCardHtml(item, snapshot, events, attestation),
|
|
493
608
|
_meta: {
|
|
494
609
|
ui: {
|
|
495
610
|
csp: {
|
|
@@ -505,10 +620,10 @@ function buildUiResource(item, snapshot, events, instance) {
|
|
|
505
620
|
// Render the declared review card from the same function used by embedded tool
|
|
506
621
|
// results, so Apps and text-first hosts cannot drift.
|
|
507
622
|
async function readQueuePanelResource(options) {
|
|
508
|
-
const { snapshot, events } = await readSessionFile(options.sessionPath);
|
|
623
|
+
const { snapshot, events, attestation } = await readSessionFile(options.sessionPath);
|
|
509
624
|
const current = currentSessionState(snapshot, events);
|
|
510
625
|
const activeItem = currentReviewItem(current);
|
|
511
|
-
return buildUiResource(activeItem, snapshot, events, "queue").resource;
|
|
626
|
+
return buildUiResource(activeItem, snapshot, events, "queue", attestation).resource;
|
|
512
627
|
}
|
|
513
628
|
// ---- Domain error (maps to isError:true, not a JSON-RPC error) -----------
|
|
514
629
|
class DomainError extends Error {
|
|
@@ -557,7 +672,7 @@ function createReviewMcpServer(options, serverVersion) {
|
|
|
557
672
|
}, async ({ itemName }) => runReviewTool(() => toolItem(itemName, options)));
|
|
558
673
|
server.registerTool("survey_review_decide", {
|
|
559
674
|
title: "Record a review decision",
|
|
560
|
-
description: "Apply a decision to a review item and persist it through Survey's server-owned validation boundary. Decision must be accept, hold, reject, or could-not-confirm. Could-not-confirm requires a reason. Domain failures return isError:true.",
|
|
675
|
+
description: "Apply a decision to a review item and persist it through Survey's server-owned validation boundary. Decision must be accept, select, hold, reject, or could-not-confirm. Select chooses one value of a conflict (several proposed values) by candidateId. Could-not-confirm requires a reason. Domain failures return isError:true.",
|
|
561
676
|
inputSchema: z.discriminatedUnion("decision", [
|
|
562
677
|
z.object({
|
|
563
678
|
itemName: z.string().min(1).describe("The ReviewItem name to decide."),
|
|
@@ -581,10 +696,23 @@ function createReviewMcpServer(options, serverVersion) {
|
|
|
581
696
|
.optional()
|
|
582
697
|
.describe("Evidence ids attempted before a could-not-confirm decision."),
|
|
583
698
|
}),
|
|
699
|
+
z.object({
|
|
700
|
+
itemName: z.string().min(1).describe("The ReviewItem name to decide."),
|
|
701
|
+
decision: z
|
|
702
|
+
.literal("select")
|
|
703
|
+
.describe("select = select-proposed: choose one value of a conflict; the other proposed values are recorded as seen and not chosen."),
|
|
704
|
+
candidateId: z
|
|
705
|
+
.string()
|
|
706
|
+
.min(1)
|
|
707
|
+
.describe("The id of the proposed candidate to use, from survey_review_item."),
|
|
708
|
+
note: z.string().optional().describe("Optional reviewer note or rationale."),
|
|
709
|
+
}),
|
|
584
710
|
]),
|
|
585
711
|
}, async (input) => runReviewTool(() => input.decision === "could-not-confirm"
|
|
586
712
|
? toolDecide(input.itemName, input.decision, input.reason, input.attemptEvidenceIds, options)
|
|
587
|
-
:
|
|
713
|
+
: input.decision === "select"
|
|
714
|
+
? toolDecide(input.itemName, input.decision, input.note, undefined, options, input.candidateId)
|
|
715
|
+
: toolDecide(input.itemName, input.decision, input.note, undefined, options)));
|
|
588
716
|
if (!options.noUi) {
|
|
589
717
|
server.registerResource("survey-review-workbench", QUEUE_PANEL_URI, {
|
|
590
718
|
title: "Survey review workbench",
|
|
@@ -200,7 +200,43 @@ export interface ReviewDecisionSpec {
|
|
|
200
200
|
* Additive/optional: absent means the candidate's original value was used unchanged.
|
|
201
201
|
*/
|
|
202
202
|
editedValue?: unknown;
|
|
203
|
+
/**
|
|
204
|
+
* The proposed candidates a `select-proposed` decision saw and did not
|
|
205
|
+
* choose, when the reviewer chose one value of a conflict (`candidateId` is
|
|
206
|
+
* the chosen one). Additive/optional: absent on every other decision, and on
|
|
207
|
+
* decisions recorded before choosing between conflicting values existed.
|
|
208
|
+
*/
|
|
209
|
+
unselectedCandidateIds?: string[];
|
|
210
|
+
/**
|
|
211
|
+
* The presentation of the session the decision was made in, copied from the
|
|
212
|
+
* session (never supplied by the reviewer). `{ scoreBlind: true }` means the
|
|
213
|
+
* reviewer saw no confidence and no verifier result before deciding.
|
|
214
|
+
* Absent on decisions from sessions that declare none.
|
|
215
|
+
*/
|
|
216
|
+
presentation?: ReviewSessionPresentation;
|
|
217
|
+
/** How the session's items were chosen, copied from the session. */
|
|
218
|
+
sampling?: ReviewSessionSampling;
|
|
203
219
|
}
|
|
220
|
+
/**
|
|
221
|
+
* How a session presents items to its reviewer, fixed by the session (never
|
|
222
|
+
* by the reviewer). `scoreBlind` hides every proposer confidence and every
|
|
223
|
+
* verifier result on every decision surface for the whole session, so the
|
|
224
|
+
* decision is made without them.
|
|
225
|
+
*/
|
|
226
|
+
export interface ReviewSessionPresentation {
|
|
227
|
+
readonly scoreBlind: boolean;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* How the session's items were chosen: the ordinary review `queue`, or a
|
|
231
|
+
* seeded `random-audit` draw at `rate` (see `drawRandomAuditSample`).
|
|
232
|
+
*/
|
|
233
|
+
export type ReviewSessionSampling = {
|
|
234
|
+
readonly kind: "queue";
|
|
235
|
+
} | {
|
|
236
|
+
readonly kind: "random-audit";
|
|
237
|
+
readonly rate: number;
|
|
238
|
+
readonly seed: string;
|
|
239
|
+
};
|
|
204
240
|
export interface ReviewDecisionStatus {
|
|
205
241
|
appliedToClaimIds?: string[];
|
|
206
242
|
}
|
|
@@ -14,6 +14,13 @@ export interface ReviewSessionFileContent {
|
|
|
14
14
|
readonly session: unknown;
|
|
15
15
|
readonly snapshot: ReviewQueueSessionState;
|
|
16
16
|
readonly events: readonly ReviewSessionEvent[];
|
|
17
|
+
/**
|
|
18
|
+
* The extraction import record (`ExtractionEnvelopeImport`) the snapshot's
|
|
19
|
+
* items were built from, stored beside them so every reload path can check
|
|
20
|
+
* the queue against it. Optional: a session without one whose items came
|
|
21
|
+
* from an import is presented as an unverified queue.
|
|
22
|
+
*/
|
|
23
|
+
readonly extractionImport?: unknown;
|
|
17
24
|
}
|
|
18
25
|
export interface ReviewSessionFileLockOptions {
|
|
19
26
|
/** How long to wait for a busy lock before failing. Defaults to 10s. */
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* it is a duplicate placement or a constant, but a key is never renamed or
|
|
26
26
|
* repointed at a different fact. Hosts may hold selectors against these values.
|
|
27
27
|
*/
|
|
28
|
-
export declare const reviewAuditRowKeys: readonly ["current-candidate-id", "proposed-candidate-id", "claim-id", "raw-source-id", "locator", "model", "extractor", "extracted-at", "excluded-proposal", "history-value", "candidate-id", "source-reference", "excerpt", "observed", "source-authority-class", "declared-by", "authority-scope", "extraction-id", "actor", "reviewed-at", "status", "rationale", "outcome", "checksum", "candidate-set-id", "authority-trace-status", "authority-trace-detail"];
|
|
28
|
+
export declare const reviewAuditRowKeys: readonly ["current-candidate-id", "proposed-candidate-id", "claim-id", "raw-source-id", "locator", "model", "extractor", "extracted-at", "excluded-proposal", "excluded-proposals-unreadable", "history-value", "candidate-id", "source-reference", "excerpt", "observed", "source-authority-class", "declared-by", "authority-scope", "extraction-id", "actor", "reviewed-at", "status", "rationale", "outcome", "checksum", "candidate-set-id", "authority-trace-status", "authority-trace-detail"];
|
|
29
29
|
/** One of {@link reviewAuditRowKeys}. */
|
|
30
30
|
export type ReviewAuditRowKey = (typeof reviewAuditRowKeys)[number];
|
|
31
31
|
/**
|