@kontourai/survey 6.0.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/candidate-verification.d.ts +175 -0
- package/dist/src/candidate-verification.js +334 -0
- package/dist/src/canonical-reviewed-trust-input.js +30 -1
- package/dist/src/console/review-console-server.js +22 -4
- package/dist/src/index.d.ts +6 -4
- package/dist/src/index.js +3 -2
- package/dist/src/mcp/review-mcp.js +161 -44
- package/dist/src/review-resource.d.ts +16 -0
- package/dist/src/review-session-file.d.ts +7 -0
- package/dist/src/review-workbench/audit-rows.d.ts +1 -1
- package/dist/src/review-workbench/audit-rows.js +1 -0
- package/dist/src/review-workbench/queue-binding.d.ts +45 -2
- package/dist/src/review-workbench/queue-binding.js +45 -0
- package/dist/src/review-workbench/review-presentation.d.ts +60 -0
- package/dist/src/review-workbench/review-presentation.js +107 -10
- package/dist/src/review-workbench/review-queue-session.d.ts +47 -4
- package/dist/src/review-workbench/review-queue-session.js +73 -8
- package/dist/src/review-workbench/review-session-replay.d.ts +11 -1
- package/dist/src/review-workbench/review-session-replay.js +35 -1
- package/dist/src/review-workbench/review-workbench-css.generated.js +96 -1
- 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 +96 -1
- package/dist/src/review-workbench/review-workbench.d.ts +28 -8
- package/dist/src/review-workbench/review-workbench.js +174 -63
- package/dist/src/review-workbench/review-workbench.standalone.css +96 -1
- 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.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export { SURVEY_INPUT_CONTRACT_VERSION } from "./types.js";
|
|
|
3
3
|
export { reviewResourceApiVersion } from "./review-resource.js";
|
|
4
4
|
export { buildReviewItemsFromExtractionEnvelopeImport, createExtractionEnvelopeResolutionIdentity, exportExtractionEnvelopeImport, extractionEnvelopeImportApiVersion, importExtractionEnvelope, portableExtractionResultFormat, portableExtractionResultVersion, reimportExtractionEnvelope, validateExtractionEnvelopeImport, } from "./extraction-envelope.js";
|
|
5
5
|
export { buildExtractionInspectorModel, exportExtractionInspector, inspectorSourcePosture, filterExtractionInspectorCandidates, } from "./review-workbench/extraction-inspector.js";
|
|
6
|
-
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./review-workbench/queue-binding.js";
|
|
7
|
-
export type { BindReviewQueueOptions, ReviewQueueBinding, ReviewQueueBindingIssue, ReviewQueueBindingIssueCode, ReviewQueueExtractionIssue, ReviewQueueExtractionIssueCode, ValidateReviewQueueBindingOptions, } from "./review-workbench/queue-binding.js";
|
|
6
|
+
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, attestReviewQueueExtraction, bindReviewQueue, reviewItemCarriesExtractionBinding, unverifiedExtractionQueueSentence, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./review-workbench/queue-binding.js";
|
|
7
|
+
export type { BindReviewQueueOptions, ReviewQueueBinding, ReviewQueueBindingIssue, ReviewQueueBindingIssueCode, ReviewQueueExtractionAttestation, ReviewQueueExtractionIssue, ReviewQueueExtractionIssueCode, ValidateReviewQueueBindingOptions, } from "./review-workbench/queue-binding.js";
|
|
8
8
|
export { resolvePortablePdfRegion } from "./pdf-layout.js";
|
|
9
9
|
export type { PortablePdfBoundingBox, PortablePdfLayout, PortablePdfPageGeometry, PortablePdfRegionContext, PortablePdfTable, PortablePdfTableCell, PortablePdfTextElement, PortablePdfTextRange, } from "./pdf-layout.js";
|
|
10
10
|
export type { ExtractionAlignmentState, ArtifactUnavailableCode, BuiltExtractionInspectorCandidate, BuiltExtractionInspectorModel, ExtractionInspectorCandidate, ExtractionInspectorEntry, ExtractionInspectorExportOptions, ExtractionInspectorFilters, ExtractionInspectorInput, ExtractionInspectorModel, ExtractionInspectorSource, ResolvedExtractionArtifact, } from "./review-workbench/extraction-inspector.js";
|
|
@@ -35,8 +35,10 @@ export { repeatedObservation } from "./repeated-observation.js";
|
|
|
35
35
|
export type { RepeatedObservationInput } from "./repeated-observation.js";
|
|
36
36
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
37
37
|
export type { SourceAuthorityClass, SourceAuthorityMetadata, SourceOfAuthorityObservationBuilderArgs, SourceOfAuthorityObservationInput, } from "./source-of-authority-observation.js";
|
|
38
|
-
export {
|
|
39
|
-
export type {
|
|
38
|
+
export { buildCandidateVerification, candidateVerificationKind, DEFAULT_SUPPORT_VERIFIER_TIMEOUT_MS, MAX_SUPPORT_VERIFIER_TIMEOUT_MS, foldCandidateVerifications, runSupportVerifier, supportVerificationInputDigest, validateCandidateVerification, valueDigest, } from "./candidate-verification.js";
|
|
39
|
+
export type { BuildCandidateVerificationInput, CandidateVerification, CandidateVerificationInapplicableReason, CandidateVerificationStatus, CandidateVerificationSubject, FoldCandidateVerificationsResult, RunSupportVerifierOptions, SupportAbstainReason, SupportEvidence, SupportVerdict, SupportVerificationInput, SupportVerificationMethod, SupportVerificationResult, SupportVerifier, } from "./candidate-verification.js";
|
|
40
|
+
export { buildInterpretationReadingPresentation, buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, candidateVerificationNotes, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
|
|
41
|
+
export type { CandidateVerificationNote, CandidateVerificationRecordPresentation, ExcludedProposalPresentation, ExcludedProposalsUnreadable, InterpretationReadingPresentation, InterpretationReadingSource, ReviewCandidatePresentation, ReviewCandidatePresentationContext, ReviewItemPresentation, ReviewItemPresentationContext, ReviewPresentationAdapter, ReviewPresentationLink, ReviewResultPresentation, ReviewTracePresentationContext, ReviewTraceRef, ReviewValuePresentationContext, } from "./review-workbench/review-presentation.js";
|
|
40
42
|
export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
|
|
41
43
|
export type { ApiRecordSourceInput, ChecksumInput, ManualEntrySourceInput, PolicyStandardMetadata, PolicyStandardSourceInput, RawSourceInput, UploadedDocumentSourceInput, WebPageSourceInput, } from "./raw-source.js";
|
|
42
44
|
export { applyAutoAcceptPolicy, applyMappingReview, buildMappingReviewItems, lookupMapping, lookupRejectedMapping, normalizeQuestion, proposalsToCandidateSet, referenceMappingProposer, resolveQuestion, } from "./inquiry-mapping.js";
|
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";
|
|
@@ -17,7 +17,8 @@ export { buildCanonicalReviewProofPayload, buildReviewProofAnchor, canonicalRevi
|
|
|
17
17
|
export { fieldObservation } from "./field-observation.js";
|
|
18
18
|
export { repeatedObservation } from "./repeated-observation.js";
|
|
19
19
|
export { sourceOfAuthorityObservation, sourceOfAuthorityObservationBuilder, SourceOfAuthorityObservationBuilder, } from "./source-of-authority-observation.js";
|
|
20
|
-
export {
|
|
20
|
+
export { buildCandidateVerification, candidateVerificationKind, DEFAULT_SUPPORT_VERIFIER_TIMEOUT_MS, MAX_SUPPORT_VERIFIER_TIMEOUT_MS, foldCandidateVerifications, runSupportVerifier, supportVerificationInputDigest, validateCandidateVerification, valueDigest, } from "./candidate-verification.js";
|
|
21
|
+
export { buildInterpretationReadingPresentation, buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, candidateVerificationNotes, humanizeIdentifier, } from "./review-workbench/review-presentation.js";
|
|
21
22
|
export { apiRecordSource, manualEntrySource, policyStandardSource, uploadedDocumentSource, webPageSource, } from "./raw-source.js";
|
|
22
23
|
export { applyAutoAcceptPolicy, applyMappingReview, buildMappingReviewItems, lookupMapping, lookupRejectedMapping, normalizeQuestion, proposalsToCandidateSet, referenceMappingProposer, resolveQuestion, } from "./inquiry-mapping.js";
|
|
23
24
|
export { referenceUtteranceExtractor, surveyAgentUtterance, utteranceToSurveyInput, } from "./agent-utterance.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, 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, 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,33 @@ 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, ``] : [];
|
|
25
43
|
}
|
|
26
44
|
// ---- Queue helpers -------------------------------------------------------
|
|
27
|
-
function queueSummaryText(snapshot, events) {
|
|
45
|
+
function queueSummaryText(snapshot, events, attestation) {
|
|
28
46
|
const current = currentSessionState(snapshot, events);
|
|
29
47
|
const summary = reviewSessionSummary(current);
|
|
30
48
|
const total = current.items.length;
|
|
@@ -38,6 +56,7 @@ function queueSummaryText(snapshot, events) {
|
|
|
38
56
|
return ` ${item.metadata.name} — ${item.spec.target} — ${status}${marker}`;
|
|
39
57
|
});
|
|
40
58
|
return [
|
|
59
|
+
...attestationLines(attestation),
|
|
41
60
|
`Review queue: ${resolved}/${total} resolved`,
|
|
42
61
|
`Active item: ${activeItem.metadata.name} (${activeItem.spec.target})`,
|
|
43
62
|
...(nextItem ? [`Next unresolved: ${nextItem}`] : ["All items resolved."]),
|
|
@@ -48,16 +67,18 @@ function queueSummaryText(snapshot, events) {
|
|
|
48
67
|
...rows,
|
|
49
68
|
].join("\n");
|
|
50
69
|
}
|
|
51
|
-
function itemDetailText(item, snapshot, events) {
|
|
70
|
+
function itemDetailText(item, snapshot, events, attestation) {
|
|
52
71
|
const current = currentSessionState(snapshot, events);
|
|
53
72
|
const status = deriveQueueRowStatus(item, current);
|
|
54
73
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
55
74
|
const note = current.notesByItemName[item.metadata.name];
|
|
75
|
+
const chosenId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
56
76
|
const currentCandidate = item.spec.candidates.find((c) => c.role === "current");
|
|
57
77
|
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
58
78
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
59
79
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "unknown";
|
|
60
80
|
const lines = [
|
|
81
|
+
...attestationLines(attestation),
|
|
61
82
|
`Item: ${item.metadata.name}`,
|
|
62
83
|
`Target: ${item.spec.target}`,
|
|
63
84
|
`Status: ${status}`,
|
|
@@ -71,27 +92,38 @@ function itemDetailText(item, snapshot, events) {
|
|
|
71
92
|
...(currentCandidate?.locator?.excerpt ? [` excerpt: ${currentCandidate.locator.excerpt}`] : []),
|
|
72
93
|
``,
|
|
73
94
|
...(proposedCandidates.length > 1
|
|
74
|
-
? [`Conflict: ${proposedCandidates.length} proposed values. Accept is refused; reject them all or use could-not-confirm with a reason.`]
|
|
95
|
+
? [`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.`]
|
|
96
|
+
: []),
|
|
97
|
+
...(chosenId !== undefined
|
|
98
|
+
? [`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
99
|
: []),
|
|
76
100
|
...(proposedCandidates.length === 0 ? [`Proposed value: (none)`] : proposedCandidates.flatMap((candidate) => [
|
|
77
|
-
`Proposed value: ${valueStr(candidate.value)}`,
|
|
101
|
+
`Proposed value: ${valueStr(candidate.value)}${chosenId === undefined ? "" : candidate.id === chosenId ? " [chosen]" : " [not chosen]"}`,
|
|
102
|
+
...(proposedCandidates.length > 1 ? [` candidateId: ${candidate.id}`] : []),
|
|
78
103
|
` confidence: ${confStr(candidate.extraction?.confidence ?? candidate.confidence)}`,
|
|
79
104
|
` source: ${candidate.source?.sourceRef ?? "none"}`,
|
|
80
105
|
...(candidate.locator?.excerpt ? [` excerpt: ${candidate.locator.excerpt}`] : []),
|
|
81
106
|
])),
|
|
82
107
|
...extractionImportLines(item),
|
|
108
|
+
...candidateVerificationNotes(item, editedValueFor(item, current)).flatMap((entry) => [``, `Verification: ${entry.sentence}`]),
|
|
83
109
|
];
|
|
84
110
|
if (item.spec.rationale) {
|
|
85
111
|
lines.push(``, `Rationale: ${item.spec.rationale}`);
|
|
86
112
|
}
|
|
87
113
|
return lines.join("\n");
|
|
88
114
|
}
|
|
115
|
+
/** The reviewer's edit when the item's decision accepts one; verifier records are read against it. */
|
|
116
|
+
function editedValueFor(item, state) {
|
|
117
|
+
return state.decisionsByItemName[item.metadata.name] === "accept-proposed" ? state.editedValuesByItemName?.[item.metadata.name] : undefined;
|
|
118
|
+
}
|
|
89
119
|
/** What an envelope import says about the item's evidence, including rival values it excluded. */
|
|
90
120
|
function extractionImportLines(item) {
|
|
91
121
|
const presentation = buildReviewItemPresentation(item);
|
|
92
122
|
const excluded = excludedProposalsSentence(presentation.excludedProposals);
|
|
123
|
+
const unreadable = excludedProposalsUnreadableSentence(presentation.excludedProposalsUnreadable);
|
|
93
124
|
return [
|
|
94
125
|
...(excluded ? [``, `Excluded: ${excluded} Check the source before accepting.`] : []),
|
|
126
|
+
...(unreadable ? [``, `Excluded: ${unreadable} Check the source before accepting.`] : []),
|
|
95
127
|
...(presentation.excerptVerification ? [``, `Excerpts ${presentation.excerptVerification === "verified" ? "were" : "were not"} checked against the prepared source text at import.`] : []),
|
|
96
128
|
];
|
|
97
129
|
}
|
|
@@ -109,17 +141,19 @@ function escapeHtml(text) {
|
|
|
109
141
|
.replace(/>/g, ">")
|
|
110
142
|
.replace(/"/g, """);
|
|
111
143
|
}
|
|
112
|
-
function buildReviewCardHtml(item, snapshot, events) {
|
|
144
|
+
function buildReviewCardHtml(item, snapshot, events, attestation) {
|
|
113
145
|
const current = currentSessionState(snapshot, events);
|
|
114
146
|
const summary = reviewSessionSummary(current);
|
|
115
147
|
const total = current.items.length;
|
|
116
148
|
const resolved = total - summary.unresolved;
|
|
117
149
|
const currentCandidate = item.spec.candidates.find((c) => c.role === "current");
|
|
118
150
|
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
119
|
-
// Several proposed values are a conflict: every value is shown,
|
|
120
|
-
// (which names a role, not a value) is not offered
|
|
151
|
+
// Several proposed values are a conflict: every value is shown, accept
|
|
152
|
+
// (which names a role, not a value) is not offered, and each value has its
|
|
153
|
+
// own "Use this value" control that names its candidate id.
|
|
121
154
|
const conflict = proposedCandidates.length > 1;
|
|
122
155
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
156
|
+
const chosenId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
123
157
|
const status = deriveQueueRowStatus(item, current);
|
|
124
158
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v, null, 2);
|
|
125
159
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "—";
|
|
@@ -130,21 +164,32 @@ function buildReviewCardHtml(item, snapshot, events) {
|
|
|
130
164
|
const proposedCard = (candidate, label) => {
|
|
131
165
|
const value = valueStr(candidate?.value ?? "—");
|
|
132
166
|
const excerpt = candidate?.locator?.excerpt ?? "";
|
|
133
|
-
|
|
167
|
+
const choice = conflict && candidate
|
|
168
|
+
? chosenId !== undefined
|
|
169
|
+
? `<div class="choice ${candidate.id === chosenId ? "chosen" : "not-chosen"}" data-candidate-id="${escapeHtml(candidate.id)}">${candidate.id === chosenId ? "Chosen" : "Not chosen"}</div>`
|
|
170
|
+
: decision === undefined
|
|
171
|
+
? `<button class="btn btn-accept btn-select" data-candidate-id="${escapeHtml(candidate.id)}">Use this value</button>`
|
|
172
|
+
: ""
|
|
173
|
+
: "";
|
|
174
|
+
return `<div class="card is-proposed"${conflict && candidate ? ` data-candidate-id="${escapeHtml(candidate.id)}"` : ""}>
|
|
134
175
|
<div class="card-label">${escapeHtml(label)}</div>
|
|
135
176
|
<div class="value">${value.includes("\n") ? `<pre>${escapeHtml(value)}</pre>` : escapeHtml(value)}</div>
|
|
136
177
|
<div class="conf">confidence ${confStr(candidate?.extraction?.confidence ?? candidate?.confidence)}</div>
|
|
137
178
|
<div class="source-ref">${escapeHtml(candidate?.source?.sourceRef ?? "—")}</div>
|
|
138
179
|
${excerpt ? `<div class="excerpt">${escapeHtml(excerpt)}</div>` : ""}
|
|
180
|
+
${choice}
|
|
139
181
|
</div>`;
|
|
140
182
|
};
|
|
141
183
|
const proposedCards = conflict
|
|
142
184
|
? proposedCandidates.map((candidate, index) => proposedCard(candidate, `Proposed ${index + 1} of ${proposedCandidates.length}`)).join("\n ")
|
|
143
185
|
: proposedCard(proposedCandidates[0], "Proposed");
|
|
144
186
|
const itemNameJson = escapeJsonInHtml(item.metadata.name);
|
|
145
|
-
const
|
|
187
|
+
const itemPresentation = buildReviewItemPresentation(item);
|
|
188
|
+
const excludedNote = excludedProposalsSentence(itemPresentation.excludedProposals);
|
|
189
|
+
const unreadableNote = excludedProposalsUnreadableSentence(itemPresentation.excludedProposalsUnreadable);
|
|
190
|
+
const verificationNotes = candidateVerificationNotes(item, editedValueFor(item, current));
|
|
146
191
|
const decisionBadge = decision
|
|
147
|
-
? `<span class="badge badge-${decision === "accept-proposed" ? "accept" : decision === "reject-proposed" ? "reject" : "hold"}">${escapeHtml(workbenchDecisionDefinitions[decision].label)}</span>`
|
|
192
|
+
? `<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>`
|
|
148
193
|
: `<span class="badge badge-pending">${escapeHtml(status)}</span>`;
|
|
149
194
|
return `<!doctype html>
|
|
150
195
|
<html lang="en">
|
|
@@ -222,9 +267,15 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
222
267
|
.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)}
|
|
223
268
|
.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)}
|
|
224
269
|
.feedback{font-size:11px;color:var(--k-text-faint);margin-top:8px;min-height:16px}
|
|
270
|
+
.btn-select{width:100%;margin-top:8px}
|
|
271
|
+
.choice{font-family:var(--k-font-mono);font-size:10px;font-weight:600;margin-top:8px;text-transform:uppercase;letter-spacing:.06em}
|
|
272
|
+
.choice.chosen{color:var(--k-positive)}
|
|
273
|
+
.choice.not-chosen{color:var(--k-text-faint)}
|
|
274
|
+
.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}
|
|
225
275
|
</style>
|
|
226
276
|
</head>
|
|
227
277
|
<body>
|
|
278
|
+
${attestation.state === "unverified" ? `<p class="notice" id="unverified-queue-note">${escapeHtml(attestation.message)}</p>` : ""}
|
|
228
279
|
<p class="eyebrow">Survey Review</p>
|
|
229
280
|
<h1>${escapeHtml(item.spec.target)}</h1>
|
|
230
281
|
<div class="meta">
|
|
@@ -245,7 +296,11 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
245
296
|
${proposedCards}
|
|
246
297
|
</div>
|
|
247
298
|
${excludedNote ? `<p class="feedback" id="excluded-note">${escapeHtml(excludedNote)}</p>` : ""}
|
|
248
|
-
${
|
|
299
|
+
${unreadableNote ? `<p class="feedback" id="excluded-unreadable-note">${escapeHtml(unreadableNote)}</p>` : ""}
|
|
300
|
+
${verificationNotes.map((entry) => `<p class="feedback verification-note" data-candidate-id="${escapeHtml(entry.candidateId)}">${escapeHtml(entry.sentence)}</p>`).join("\n")}
|
|
301
|
+
${conflict ? `<p class="feedback" id="conflict-note">${escapeHtml(chosenId !== undefined
|
|
302
|
+
? `${proposedCandidates.length} different values were proposed. One was chosen; ${proposedCandidates.length === 2 ? "the other was" : "the others were"} seen and not chosen.`
|
|
303
|
+
: `${proposedCandidates.length} different values were proposed. Use the value the source supports, reject them all, or use Could not confirm with a reason.`)}</p>` : ""}
|
|
249
304
|
|
|
250
305
|
<div class="divider"></div>
|
|
251
306
|
|
|
@@ -265,7 +320,7 @@ ${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length
|
|
|
265
320
|
var itemName = ${itemNameJson};
|
|
266
321
|
var msgId = 1;
|
|
267
322
|
|
|
268
|
-
function postDecision(decision) {
|
|
323
|
+
function postDecision(decision, candidateId) {
|
|
269
324
|
var note = document.getElementById('note').value;
|
|
270
325
|
if (decision === 'could-not-confirm' && !note.trim()) {
|
|
271
326
|
document.getElementById('feedback').textContent = 'A reason is required when you could not confirm.';
|
|
@@ -280,12 +335,18 @@ ${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length
|
|
|
280
335
|
name: "survey_review_decide",
|
|
281
336
|
arguments: decision === 'could-not-confirm'
|
|
282
337
|
? { itemName: itemName, decision: decision, reason: note }
|
|
283
|
-
:
|
|
338
|
+
: decision === 'select'
|
|
339
|
+
? { itemName: itemName, decision: decision, candidateId: candidateId, note: note || undefined }
|
|
340
|
+
: { itemName: itemName, decision: decision, note: note || undefined }
|
|
284
341
|
}
|
|
285
342
|
}, "*");
|
|
286
343
|
return true;
|
|
287
344
|
}
|
|
288
345
|
|
|
346
|
+
Array.prototype.forEach.call(document.querySelectorAll('.btn-select'), function (button) {
|
|
347
|
+
button.addEventListener('click', function () { postDecision('select', button.getAttribute('data-candidate-id')); document.getElementById('feedback').textContent = 'Submitting choice…'; });
|
|
348
|
+
});
|
|
349
|
+
|
|
289
350
|
var acceptButton = document.getElementById('btn-accept');
|
|
290
351
|
if (acceptButton) acceptButton.addEventListener('click', function () { postDecision('accept'); document.getElementById('feedback').textContent = 'Submitting accept…'; });
|
|
291
352
|
var holdButton = document.getElementById('btn-hold');
|
|
@@ -311,16 +372,21 @@ ${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length
|
|
|
311
372
|
// ---- Tool implementations ------------------------------------------------
|
|
312
373
|
async function toolQueue(options) {
|
|
313
374
|
const file = await readSessionFile(options.sessionPath);
|
|
314
|
-
const { snapshot, events } = file;
|
|
315
|
-
const text = queueSummaryText(snapshot, events);
|
|
375
|
+
const { snapshot, events, attestation } = file;
|
|
376
|
+
const text = queueSummaryText(snapshot, events, attestation);
|
|
316
377
|
const queueData = {
|
|
378
|
+
queueAttestation: attestation.state,
|
|
317
379
|
items: snapshot.items.map((item) => {
|
|
318
380
|
const current = currentSessionState(snapshot, events);
|
|
381
|
+
const selectedCandidateId = current.decisionsByItemName[item.metadata.name] === "select-proposed"
|
|
382
|
+
? current.selectedCandidateIdsByItemName?.[item.metadata.name]
|
|
383
|
+
: undefined;
|
|
319
384
|
return {
|
|
320
385
|
name: item.metadata.name,
|
|
321
386
|
target: item.spec.target,
|
|
322
387
|
status: deriveQueueRowStatus(item, current),
|
|
323
388
|
decision: current.decisionsByItemName[item.metadata.name],
|
|
389
|
+
...(selectedCandidateId !== undefined ? { selectedCandidateId } : {}),
|
|
324
390
|
candidateSetStatus: item.spec.candidateSetStatus,
|
|
325
391
|
};
|
|
326
392
|
}),
|
|
@@ -332,24 +398,28 @@ async function toolQueue(options) {
|
|
|
332
398
|
];
|
|
333
399
|
if (!options.noUi) {
|
|
334
400
|
const activeItem = currentReviewItem(currentSessionState(snapshot, events));
|
|
335
|
-
content.push(buildUiResource(activeItem, snapshot, events, "queue"));
|
|
401
|
+
content.push(buildUiResource(activeItem, snapshot, events, "queue", attestation));
|
|
336
402
|
}
|
|
337
403
|
return content;
|
|
338
404
|
}
|
|
339
405
|
async function toolItem(itemName, options) {
|
|
340
406
|
const file = await readSessionFile(options.sessionPath);
|
|
341
|
-
const { snapshot, events } = file;
|
|
407
|
+
const { snapshot, events, attestation } = file;
|
|
342
408
|
const current = currentSessionState(snapshot, events);
|
|
343
409
|
const item = current.items.find((i) => i.metadata.name === itemName);
|
|
344
410
|
if (!item) {
|
|
345
411
|
throw new DomainError(`Unknown review item: ${itemName}`);
|
|
346
412
|
}
|
|
347
|
-
const text = itemDetailText(item, snapshot, events);
|
|
413
|
+
const text = itemDetailText(item, snapshot, events, attestation);
|
|
414
|
+
const decision = current.decisionsByItemName[item.metadata.name];
|
|
415
|
+
const selectedCandidateId = decision === "select-proposed" ? current.selectedCandidateIdsByItemName?.[item.metadata.name] : undefined;
|
|
348
416
|
const itemData = {
|
|
417
|
+
queueAttestation: attestation.state,
|
|
349
418
|
name: item.metadata.name,
|
|
350
419
|
target: item.spec.target,
|
|
351
420
|
status: deriveQueueRowStatus(item, current),
|
|
352
|
-
decision
|
|
421
|
+
decision,
|
|
422
|
+
...(selectedCandidateId !== undefined ? { selectedCandidateId } : {}),
|
|
353
423
|
note: current.notesByItemName[item.metadata.name],
|
|
354
424
|
candidateSetStatus: item.spec.candidateSetStatus,
|
|
355
425
|
...(() => {
|
|
@@ -357,37 +427,47 @@ async function toolItem(itemName, options) {
|
|
|
357
427
|
return {
|
|
358
428
|
...(presentation.excerptVerification ? { excerptVerification: presentation.excerptVerification } : {}),
|
|
359
429
|
...(presentation.excludedProposals.length ? { excludedProposals: presentation.excludedProposals.map(({ proposalIndex, value, locator, excerpt }) => ({ proposalIndex, value, locator, excerpt })) } : {}),
|
|
430
|
+
...(presentation.excludedProposalsUnreadable ? { excludedProposalsUnreadable: presentation.excludedProposalsUnreadable } : {}),
|
|
360
431
|
};
|
|
361
432
|
})(),
|
|
362
|
-
candidates:
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
433
|
+
candidates: (() => {
|
|
434
|
+
const notes = candidateVerificationNotes(item, editedValueFor(item, current));
|
|
435
|
+
return item.spec.candidates.map((c, index) => {
|
|
436
|
+
const note = notes.find((entry) => entry.candidateIndex === index);
|
|
437
|
+
return {
|
|
438
|
+
id: c.id,
|
|
439
|
+
role: c.role,
|
|
440
|
+
value: c.value,
|
|
441
|
+
confidence: c.extraction?.confidence ?? c.confidence,
|
|
442
|
+
sourceRef: c.source?.sourceRef,
|
|
443
|
+
excerpt: c.locator?.excerpt,
|
|
444
|
+
...(selectedCandidateId !== undefined && c.role === "proposed" ? { chosen: c.id === selectedCandidateId } : {}),
|
|
445
|
+
...(note ? { verification: { subject: note.subjectLabel, status: note.status, records: note.records, inapplicableCount: note.inapplicableCount, rejectedCount: note.rejectedCount } } : {}),
|
|
446
|
+
};
|
|
447
|
+
});
|
|
448
|
+
})(),
|
|
370
449
|
};
|
|
371
450
|
const content = [
|
|
372
451
|
{ type: "text", text: `${text}\n\n${JSON.stringify(itemData, null, 2)}` },
|
|
373
452
|
];
|
|
374
453
|
if (!options.noUi) {
|
|
375
|
-
content.push(buildUiResource(item, snapshot, events, itemName));
|
|
454
|
+
content.push(buildUiResource(item, snapshot, events, itemName, attestation));
|
|
376
455
|
}
|
|
377
456
|
return content;
|
|
378
457
|
}
|
|
379
|
-
async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, options) {
|
|
458
|
+
async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, options, selectedCandidateId) {
|
|
380
459
|
const wbDecision = MCP_DECISION_MAP[mcpDecision];
|
|
381
460
|
if (!wbDecision) {
|
|
382
|
-
throw new DomainError(`Invalid decision: ${mcpDecision}. Must be accept, hold, reject, or could-not-confirm.`);
|
|
461
|
+
throw new DomainError(`Invalid decision: ${mcpDecision}. Must be accept, select, hold, reject, or could-not-confirm.`);
|
|
383
462
|
}
|
|
384
463
|
if (wbDecision === "could-not-confirm" && !note?.trim()) {
|
|
385
464
|
throw new DomainError("survey_review_decide requires a non-empty reason for could-not-confirm");
|
|
386
465
|
}
|
|
387
466
|
// Read, validate and write inside the shared session lock so a concurrent
|
|
388
467
|
// decide or console save cannot interleave and drop this decision (#281).
|
|
389
|
-
const { snapshot, sessionWithDecision, newEvents } = await updateReviewSessionFile(options.sessionPath, (file) => {
|
|
468
|
+
const { snapshot, sessionWithDecision, newEvents, attestation } = await updateReviewSessionFile(options.sessionPath, (file) => {
|
|
390
469
|
const { snapshot, events } = file;
|
|
470
|
+
const attestation = attestedQueue(file);
|
|
391
471
|
const current = currentSessionState(snapshot, events);
|
|
392
472
|
const item = current.items.find((i) => i.metadata.name === itemName);
|
|
393
473
|
if (!item) {
|
|
@@ -397,6 +477,12 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
397
477
|
if (existingDecision) {
|
|
398
478
|
throw new DomainError(`Item ${itemName} already has a decision: ${existingDecision}. Use a new session to re-decide.`);
|
|
399
479
|
}
|
|
480
|
+
if (wbDecision === "select-proposed") {
|
|
481
|
+
const selectionIssue = conflictSelectionIssue(item, selectedCandidateId);
|
|
482
|
+
if (selectionIssue) {
|
|
483
|
+
throw new DomainError(selectionIssue);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
400
486
|
// Build the updated session state with the decision
|
|
401
487
|
const sessionWithDecision = {
|
|
402
488
|
...current,
|
|
@@ -420,6 +506,14 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
420
506
|
},
|
|
421
507
|
}
|
|
422
508
|
: {}),
|
|
509
|
+
...(wbDecision === "select-proposed" && selectedCandidateId !== undefined
|
|
510
|
+
? {
|
|
511
|
+
selectedCandidateIdsByItemName: {
|
|
512
|
+
...current.selectedCandidateIdsByItemName,
|
|
513
|
+
[itemName]: selectedCandidateId,
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
: {}),
|
|
423
517
|
};
|
|
424
518
|
// Append only this decision's events (its note, then the decision) to the
|
|
425
519
|
// stored log. Regenerating the whole log from state would erase earlier
|
|
@@ -441,26 +535,36 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
441
535
|
record,
|
|
442
536
|
events: newEvents,
|
|
443
537
|
requiredResolvedItems: "none",
|
|
538
|
+
extractionImport: file.extractionImport,
|
|
444
539
|
});
|
|
445
540
|
if (!applyResult.ok) {
|
|
446
541
|
throw new DomainError(`Decision validation failed: ${applyResult.issues.map((issue) => "message" in issue ? issue.message : String(issue)).join("; ")}`);
|
|
447
542
|
}
|
|
448
543
|
const updatedFile = {
|
|
544
|
+
...file,
|
|
449
545
|
session: file.session,
|
|
450
546
|
snapshot,
|
|
451
547
|
events: newEvents,
|
|
452
548
|
};
|
|
453
|
-
return { next: updatedFile, result: { snapshot, sessionWithDecision, newEvents } };
|
|
549
|
+
return { next: updatedFile, result: { snapshot, sessionWithDecision, newEvents, attestation } };
|
|
454
550
|
});
|
|
455
551
|
// Summarize the result
|
|
456
552
|
const updatedItem = sessionWithDecision.items.find((i) => i.metadata.name === itemName);
|
|
457
|
-
const itemText = updatedItem ? itemDetailText(updatedItem, snapshot, newEvents) : `Item: ${itemName}`;
|
|
458
|
-
const remainingText = queueSummaryText(snapshot, newEvents);
|
|
553
|
+
const itemText = updatedItem ? itemDetailText(updatedItem, snapshot, newEvents, attestation) : `Item: ${itemName}`;
|
|
554
|
+
const remainingText = queueSummaryText(snapshot, newEvents, attestation);
|
|
459
555
|
const definition = workbenchDecisionDefinitions[wbDecision];
|
|
460
556
|
const conflictRejected = wbDecision === "reject-proposed" && updatedItem !== undefined && decisionSelectsNoCandidate(updatedItem, wbDecision);
|
|
557
|
+
const valueStr = (v) => (typeof v === "string" ? v : JSON.stringify(v));
|
|
558
|
+
const proposed = updatedItem?.spec.candidates.filter((c) => c.role === "proposed") ?? [];
|
|
559
|
+
const chosen = proposed.find((c) => c.id === selectedCandidateId);
|
|
560
|
+
const effect = conflictRejected
|
|
561
|
+
? "Every proposed value is rejected; none becomes the claim's value."
|
|
562
|
+
: wbDecision === "select-proposed"
|
|
563
|
+
? `${valueStr(chosen?.value)} becomes the verified value; not chosen: ${proposed.filter((c) => c.id !== selectedCandidateId).map((c) => valueStr(c.value)).join(", ")}.`
|
|
564
|
+
: definition.effect;
|
|
461
565
|
const text = [
|
|
462
566
|
`Decision recorded: ${conflictRejected ? "Reject all values" : definition.label}`,
|
|
463
|
-
`Effect: ${
|
|
567
|
+
`Effect: ${effect}`,
|
|
464
568
|
"",
|
|
465
569
|
itemText,
|
|
466
570
|
"",
|
|
@@ -469,13 +573,13 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
469
573
|
].join("\n");
|
|
470
574
|
return [{ type: "text", text }];
|
|
471
575
|
}
|
|
472
|
-
function buildUiResource(item, snapshot, events, instance) {
|
|
576
|
+
function buildUiResource(item, snapshot, events, instance, attestation) {
|
|
473
577
|
return {
|
|
474
578
|
type: "resource",
|
|
475
579
|
resource: {
|
|
476
580
|
uri: `ui://survey/review-card/${encodeURIComponent(instance)}`,
|
|
477
581
|
mimeType: "text/html;profile=mcp-app",
|
|
478
|
-
text: buildReviewCardHtml(item, snapshot, events),
|
|
582
|
+
text: buildReviewCardHtml(item, snapshot, events, attestation),
|
|
479
583
|
_meta: {
|
|
480
584
|
ui: {
|
|
481
585
|
csp: {
|
|
@@ -491,10 +595,10 @@ function buildUiResource(item, snapshot, events, instance) {
|
|
|
491
595
|
// Render the declared review card from the same function used by embedded tool
|
|
492
596
|
// results, so Apps and text-first hosts cannot drift.
|
|
493
597
|
async function readQueuePanelResource(options) {
|
|
494
|
-
const { snapshot, events } = await readSessionFile(options.sessionPath);
|
|
598
|
+
const { snapshot, events, attestation } = await readSessionFile(options.sessionPath);
|
|
495
599
|
const current = currentSessionState(snapshot, events);
|
|
496
600
|
const activeItem = currentReviewItem(current);
|
|
497
|
-
return buildUiResource(activeItem, snapshot, events, "queue").resource;
|
|
601
|
+
return buildUiResource(activeItem, snapshot, events, "queue", attestation).resource;
|
|
498
602
|
}
|
|
499
603
|
// ---- Domain error (maps to isError:true, not a JSON-RPC error) -----------
|
|
500
604
|
class DomainError extends Error {
|
|
@@ -543,7 +647,7 @@ function createReviewMcpServer(options, serverVersion) {
|
|
|
543
647
|
}, async ({ itemName }) => runReviewTool(() => toolItem(itemName, options)));
|
|
544
648
|
server.registerTool("survey_review_decide", {
|
|
545
649
|
title: "Record a review decision",
|
|
546
|
-
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.",
|
|
650
|
+
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.",
|
|
547
651
|
inputSchema: z.discriminatedUnion("decision", [
|
|
548
652
|
z.object({
|
|
549
653
|
itemName: z.string().min(1).describe("The ReviewItem name to decide."),
|
|
@@ -567,10 +671,23 @@ function createReviewMcpServer(options, serverVersion) {
|
|
|
567
671
|
.optional()
|
|
568
672
|
.describe("Evidence ids attempted before a could-not-confirm decision."),
|
|
569
673
|
}),
|
|
674
|
+
z.object({
|
|
675
|
+
itemName: z.string().min(1).describe("The ReviewItem name to decide."),
|
|
676
|
+
decision: z
|
|
677
|
+
.literal("select")
|
|
678
|
+
.describe("select = select-proposed: choose one value of a conflict; the other proposed values are recorded as seen and not chosen."),
|
|
679
|
+
candidateId: z
|
|
680
|
+
.string()
|
|
681
|
+
.min(1)
|
|
682
|
+
.describe("The id of the proposed candidate to use, from survey_review_item."),
|
|
683
|
+
note: z.string().optional().describe("Optional reviewer note or rationale."),
|
|
684
|
+
}),
|
|
570
685
|
]),
|
|
571
686
|
}, async (input) => runReviewTool(() => input.decision === "could-not-confirm"
|
|
572
687
|
? toolDecide(input.itemName, input.decision, input.reason, input.attemptEvidenceIds, options)
|
|
573
|
-
:
|
|
688
|
+
: input.decision === "select"
|
|
689
|
+
? toolDecide(input.itemName, input.decision, input.note, undefined, options, input.candidateId)
|
|
690
|
+
: toolDecide(input.itemName, input.decision, input.note, undefined, options)));
|
|
574
691
|
if (!options.noUi) {
|
|
575
692
|
server.registerResource("survey-review-workbench", QUEUE_PANEL_URI, {
|
|
576
693
|
title: "Survey review workbench",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CandidateVerification } from "./candidate-verification.js";
|
|
1
2
|
import type { CandidateSetStatus, ClaimTarget, Extraction, RawSource, ReviewAuthorizing, ReviewOutcome } from "./types.js";
|
|
2
3
|
export declare const reviewResourceApiVersion = "survey.kontourai.io/v1alpha1";
|
|
3
4
|
export type ReviewResourceApiVersion = typeof reviewResourceApiVersion;
|
|
@@ -104,6 +105,14 @@ export interface ReviewCandidate {
|
|
|
104
105
|
claimTarget: ClaimTargetHint;
|
|
105
106
|
projection?: SurveyRecordProjectionHint;
|
|
106
107
|
producer?: Record<string, unknown>;
|
|
108
|
+
/**
|
|
109
|
+
* Records of what independent support verifiers said about this candidate's
|
|
110
|
+
* value (see `CandidateVerification`). A record of a verifier's output, not a
|
|
111
|
+
* trust decision: nothing routes, accepts, or calibrates on it. Every decision
|
|
112
|
+
* surface validates each record on read, ignores any that fail, and shows only
|
|
113
|
+
* those bound to the value under review.
|
|
114
|
+
*/
|
|
115
|
+
verifications?: CandidateVerification[];
|
|
107
116
|
}
|
|
108
117
|
/**
|
|
109
118
|
* Well-known neutral value-type vocabulary for {@link ReviewValueDescriptor}.
|
|
@@ -191,6 +200,13 @@ export interface ReviewDecisionSpec {
|
|
|
191
200
|
* Additive/optional: absent means the candidate's original value was used unchanged.
|
|
192
201
|
*/
|
|
193
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[];
|
|
194
210
|
}
|
|
195
211
|
export interface ReviewDecisionStatus {
|
|
196
212
|
appliedToClaimIds?: string[];
|
|
@@ -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
|
/**
|