@kontourai/survey 4.0.0 → 6.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/examples/review-workbench/server-apply-consumer.js +5 -0
- package/dist/src/canonical-reviewed-trust-input.js +73 -34
- package/dist/src/extraction-envelope.d.ts +125 -3
- package/dist/src/extraction-envelope.js +332 -25
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/review-mcp.js +62 -25
- 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/extraction-inspector.d.ts +31 -13
- package/dist/src/review-workbench/extraction-inspector.js +77 -47
- package/dist/src/review-workbench/queue-binding.js +1 -1
- package/dist/src/review-workbench/review-presentation.d.ts +26 -1
- package/dist/src/review-workbench/review-presentation.js +75 -1
- package/dist/src/review-workbench/review-queue-session.d.ts +19 -5
- package/dist/src/review-workbench/review-queue-session.js +46 -13
- package/dist/src/review-workbench/review-session-replay.js +5 -3
- package/dist/src/review-workbench/review-workbench-css.generated.js +10 -1
- package/dist/src/review-workbench/review-workbench.css +10 -1
- package/dist/src/review-workbench/review-workbench.d.ts +14 -7
- package/dist/src/review-workbench/review-workbench.js +111 -20
- package/dist/src/review-workbench/review-workbench.standalone.css +10 -1
- package/dist/src/surface-reviewed-extraction.js +4 -0
- package/dist/src/to-surface.js +116 -0
- package/dist/src/types.d.ts +5 -1
- package/package.json +1 -1
|
@@ -3,8 +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, deriveQueueRowStatus, nextUnresolvedItemName, reviewSessionSummary, workbenchDecisionDefinitions, } from "../review-workbench/review-workbench.js";
|
|
6
|
+
import { buildReviewSessionEvents, 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
9
|
import { appendReviewSessionEvents, readReviewSessionFile, storedReviewSessionName, updateReviewSessionFile, } from "../review-session-file.js";
|
|
9
10
|
const SESSION_NAME = "mcp-review-session";
|
|
10
11
|
const UI_RESOURCE_URI_META_KEY = "ui/resourceUri";
|
|
@@ -53,7 +54,7 @@ function itemDetailText(item, snapshot, events) {
|
|
|
53
54
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
54
55
|
const note = current.notesByItemName[item.metadata.name];
|
|
55
56
|
const currentCandidate = item.spec.candidates.find((c) => c.role === "current");
|
|
56
|
-
const
|
|
57
|
+
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
57
58
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v);
|
|
58
59
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "unknown";
|
|
59
60
|
const lines = [
|
|
@@ -69,16 +70,31 @@ function itemDetailText(item, snapshot, events) {
|
|
|
69
70
|
` source: ${currentCandidate?.source?.sourceRef ?? "none"}`,
|
|
70
71
|
...(currentCandidate?.locator?.excerpt ? [` excerpt: ${currentCandidate.locator.excerpt}`] : []),
|
|
71
72
|
``,
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
...(
|
|
73
|
+
...(proposedCandidates.length > 1
|
|
74
|
+
? [`Conflict: ${proposedCandidates.length} proposed values. Accept is refused; reject them all or use could-not-confirm with a reason.`]
|
|
75
|
+
: []),
|
|
76
|
+
...(proposedCandidates.length === 0 ? [`Proposed value: (none)`] : proposedCandidates.flatMap((candidate) => [
|
|
77
|
+
`Proposed value: ${valueStr(candidate.value)}`,
|
|
78
|
+
` confidence: ${confStr(candidate.extraction?.confidence ?? candidate.confidence)}`,
|
|
79
|
+
` source: ${candidate.source?.sourceRef ?? "none"}`,
|
|
80
|
+
...(candidate.locator?.excerpt ? [` excerpt: ${candidate.locator.excerpt}`] : []),
|
|
81
|
+
])),
|
|
82
|
+
...extractionImportLines(item),
|
|
76
83
|
];
|
|
77
84
|
if (item.spec.rationale) {
|
|
78
85
|
lines.push(``, `Rationale: ${item.spec.rationale}`);
|
|
79
86
|
}
|
|
80
87
|
return lines.join("\n");
|
|
81
88
|
}
|
|
89
|
+
/** What an envelope import says about the item's evidence, including rival values it excluded. */
|
|
90
|
+
function extractionImportLines(item) {
|
|
91
|
+
const presentation = buildReviewItemPresentation(item);
|
|
92
|
+
const excluded = excludedProposalsSentence(presentation.excludedProposals);
|
|
93
|
+
return [
|
|
94
|
+
...(excluded ? [``, `Excluded: ${excluded} Check the source before accepting.`] : []),
|
|
95
|
+
...(presentation.excerptVerification ? [``, `Excerpts ${presentation.excerptVerification === "verified" ? "were" : "were not"} checked against the prepared source text at import.`] : []),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
82
98
|
// ---- UI card -------------------------------------------------------------
|
|
83
99
|
function escapeJsonInHtml(value) {
|
|
84
100
|
return JSON.stringify(value)
|
|
@@ -99,20 +115,34 @@ function buildReviewCardHtml(item, snapshot, events) {
|
|
|
99
115
|
const total = current.items.length;
|
|
100
116
|
const resolved = total - summary.unresolved;
|
|
101
117
|
const currentCandidate = item.spec.candidates.find((c) => c.role === "current");
|
|
102
|
-
const
|
|
118
|
+
const proposedCandidates = item.spec.candidates.filter((c) => c.role === "proposed");
|
|
119
|
+
// Several proposed values are a conflict: every value is shown, and accept
|
|
120
|
+
// (which names a role, not a value) is not offered.
|
|
121
|
+
const conflict = proposedCandidates.length > 1;
|
|
103
122
|
const decision = current.decisionsByItemName[item.metadata.name];
|
|
104
123
|
const status = deriveQueueRowStatus(item, current);
|
|
105
124
|
const valueStr = (v) => typeof v === "string" ? v : JSON.stringify(v, null, 2);
|
|
106
125
|
const confStr = (c) => c !== undefined ? `${Math.round(c * 100)}%` : "—";
|
|
107
126
|
const currentValue = valueStr(currentCandidate?.value ?? "—");
|
|
108
|
-
const proposedValue = valueStr(proposedCandidate?.value ?? "—");
|
|
109
127
|
const currentConf = confStr(currentCandidate?.extraction?.confidence ?? currentCandidate?.confidence);
|
|
110
|
-
const proposedConf = confStr(proposedCandidate?.extraction?.confidence ?? proposedCandidate?.confidence);
|
|
111
128
|
const currentSource = currentCandidate?.source?.sourceRef ?? "—";
|
|
112
|
-
const proposedSource = proposedCandidate?.source?.sourceRef ?? "—";
|
|
113
129
|
const currentExcerpt = currentCandidate?.locator?.excerpt ?? "";
|
|
114
|
-
const
|
|
130
|
+
const proposedCard = (candidate, label) => {
|
|
131
|
+
const value = valueStr(candidate?.value ?? "—");
|
|
132
|
+
const excerpt = candidate?.locator?.excerpt ?? "";
|
|
133
|
+
return `<div class="card is-proposed">
|
|
134
|
+
<div class="card-label">${escapeHtml(label)}</div>
|
|
135
|
+
<div class="value">${value.includes("\n") ? `<pre>${escapeHtml(value)}</pre>` : escapeHtml(value)}</div>
|
|
136
|
+
<div class="conf">confidence ${confStr(candidate?.extraction?.confidence ?? candidate?.confidence)}</div>
|
|
137
|
+
<div class="source-ref">${escapeHtml(candidate?.source?.sourceRef ?? "—")}</div>
|
|
138
|
+
${excerpt ? `<div class="excerpt">${escapeHtml(excerpt)}</div>` : ""}
|
|
139
|
+
</div>`;
|
|
140
|
+
};
|
|
141
|
+
const proposedCards = conflict
|
|
142
|
+
? proposedCandidates.map((candidate, index) => proposedCard(candidate, `Proposed ${index + 1} of ${proposedCandidates.length}`)).join("\n ")
|
|
143
|
+
: proposedCard(proposedCandidates[0], "Proposed");
|
|
115
144
|
const itemNameJson = escapeJsonInHtml(item.metadata.name);
|
|
145
|
+
const excludedNote = excludedProposalsSentence(buildReviewItemPresentation(item).excludedProposals);
|
|
116
146
|
const decisionBadge = decision
|
|
117
147
|
? `<span class="badge badge-${decision === "accept-proposed" ? "accept" : decision === "reject-proposed" ? "reject" : "hold"}">${escapeHtml(workbenchDecisionDefinitions[decision].label)}</span>`
|
|
118
148
|
: `<span class="badge badge-pending">${escapeHtml(status)}</span>`;
|
|
@@ -200,6 +230,7 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
200
230
|
<div class="meta">
|
|
201
231
|
<span>${escapeHtml(item.metadata.name)}</span>
|
|
202
232
|
${decisionBadge}
|
|
233
|
+
${conflict ? `<span class="badge badge-hold" id="conflict-badge">Conflict: ${proposedCandidates.length} values</span>` : ""}
|
|
203
234
|
<span class="progress">${resolved}/${total} resolved</span>
|
|
204
235
|
</div>
|
|
205
236
|
|
|
@@ -211,14 +242,10 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
211
242
|
<div class="source-ref">${escapeHtml(currentSource)}</div>
|
|
212
243
|
${currentExcerpt ? `<div class="excerpt">${escapeHtml(currentExcerpt)}</div>` : ""}
|
|
213
244
|
</div>
|
|
214
|
-
|
|
215
|
-
<div class="card-label">Proposed</div>
|
|
216
|
-
<div class="value">${proposedValue.includes("\n") ? `<pre>${escapeHtml(proposedValue)}</pre>` : escapeHtml(proposedValue)}</div>
|
|
217
|
-
<div class="conf">confidence ${proposedConf}</div>
|
|
218
|
-
<div class="source-ref">${escapeHtml(proposedSource)}</div>
|
|
219
|
-
${proposedExcerpt ? `<div class="excerpt">${escapeHtml(proposedExcerpt)}</div>` : ""}
|
|
220
|
-
</div>
|
|
245
|
+
${proposedCards}
|
|
221
246
|
</div>
|
|
247
|
+
${excludedNote ? `<p class="feedback" id="excluded-note">${escapeHtml(excludedNote)}</p>` : ""}
|
|
248
|
+
${conflict ? `<p class="feedback" id="conflict-note">${proposedCandidates.length} different values were proposed. This card cannot choose one of them yet: reject them all, or use Could not confirm with a reason.</p>` : ""}
|
|
222
249
|
|
|
223
250
|
<div class="divider"></div>
|
|
224
251
|
|
|
@@ -226,9 +253,9 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
226
253
|
<textarea class="note-input" id="note" placeholder="Add a rationale for this decision...">${escapeHtml(current.notesByItemName[item.metadata.name] ?? "")}</textarea>
|
|
227
254
|
|
|
228
255
|
<div class="btn-row">
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<button class="btn btn-reject${decision === "reject-proposed" ? " active" : ""}" id="btn-reject"
|
|
256
|
+
${conflict ? "" : `<button class="btn btn-accept${decision === "accept-proposed" ? " active" : ""}" id="btn-accept">Accept proposed</button>`}
|
|
257
|
+
${currentCandidate ? `<button class="btn btn-hold${decision === "keep-current" ? " active" : ""}" id="btn-hold">Hold / Keep current</button>` : ""}
|
|
258
|
+
<button class="btn btn-reject${decision === "reject-proposed" ? " active" : ""}" id="btn-reject">${conflict ? "Reject all values" : "Reject proposed"}</button>
|
|
232
259
|
<button class="btn btn-unconfirmed${decision === "could-not-confirm" ? " active" : ""}" id="btn-unconfirmed">Could not confirm</button>
|
|
233
260
|
</div>
|
|
234
261
|
<div class="feedback" id="feedback"></div>
|
|
@@ -259,8 +286,10 @@ h1{font-size:15px;font-weight:700;margin:0 0 4px}
|
|
|
259
286
|
return true;
|
|
260
287
|
}
|
|
261
288
|
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
var acceptButton = document.getElementById('btn-accept');
|
|
290
|
+
if (acceptButton) acceptButton.addEventListener('click', function () { postDecision('accept'); document.getElementById('feedback').textContent = 'Submitting accept…'; });
|
|
291
|
+
var holdButton = document.getElementById('btn-hold');
|
|
292
|
+
if (holdButton) holdButton.addEventListener('click', function () { postDecision('hold'); document.getElementById('feedback').textContent = 'Submitting hold…'; });
|
|
264
293
|
document.getElementById('btn-reject').addEventListener('click', function () { postDecision('reject'); document.getElementById('feedback').textContent = 'Submitting reject…'; });
|
|
265
294
|
document.getElementById('btn-unconfirmed').addEventListener('click', function () { if (postDecision('could-not-confirm')) document.getElementById('feedback').textContent = 'Submitting could not confirm…'; });
|
|
266
295
|
|
|
@@ -323,6 +352,13 @@ async function toolItem(itemName, options) {
|
|
|
323
352
|
decision: current.decisionsByItemName[item.metadata.name],
|
|
324
353
|
note: current.notesByItemName[item.metadata.name],
|
|
325
354
|
candidateSetStatus: item.spec.candidateSetStatus,
|
|
355
|
+
...(() => {
|
|
356
|
+
const presentation = buildReviewItemPresentation(item);
|
|
357
|
+
return {
|
|
358
|
+
...(presentation.excerptVerification ? { excerptVerification: presentation.excerptVerification } : {}),
|
|
359
|
+
...(presentation.excludedProposals.length ? { excludedProposals: presentation.excludedProposals.map(({ proposalIndex, value, locator, excerpt }) => ({ proposalIndex, value, locator, excerpt })) } : {}),
|
|
360
|
+
};
|
|
361
|
+
})(),
|
|
326
362
|
candidates: item.spec.candidates.map((c) => ({
|
|
327
363
|
id: c.id,
|
|
328
364
|
role: c.role,
|
|
@@ -421,9 +457,10 @@ async function toolDecide(itemName, mcpDecision, note, attemptEvidenceIds, optio
|
|
|
421
457
|
const itemText = updatedItem ? itemDetailText(updatedItem, snapshot, newEvents) : `Item: ${itemName}`;
|
|
422
458
|
const remainingText = queueSummaryText(snapshot, newEvents);
|
|
423
459
|
const definition = workbenchDecisionDefinitions[wbDecision];
|
|
460
|
+
const conflictRejected = wbDecision === "reject-proposed" && updatedItem !== undefined && decisionSelectsNoCandidate(updatedItem, wbDecision);
|
|
424
461
|
const text = [
|
|
425
|
-
`Decision recorded: ${definition.label}`,
|
|
426
|
-
`Effect: ${definition.effect}`,
|
|
462
|
+
`Decision recorded: ${conflictRejected ? "Reject all values" : definition.label}`,
|
|
463
|
+
`Effect: ${conflictRejected ? "Every proposed value is rejected; none becomes the claim's value." : definition.effect}`,
|
|
427
464
|
"",
|
|
428
465
|
itemText,
|
|
429
466
|
"",
|
|
@@ -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", "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", "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
|
/**
|
|
@@ -1,18 +1,7 @@
|
|
|
1
|
-
import { type ExtractionEnvelopeImportResult } from "../extraction-envelope.js";
|
|
1
|
+
import { type ExtractionEnvelopeImportDiagnostic, type ExtractionEnvelopeImportResult, type ResolvedExtractionArtifact } from "../extraction-envelope.js";
|
|
2
2
|
import { type PortablePdfRegionContext } from "../pdf-layout.js";
|
|
3
3
|
export type ExtractionAlignmentState = "aligned" | "excerpt-mismatch" | "artifact-unavailable" | "digest-mismatch";
|
|
4
|
-
export type ArtifactUnavailableCode
|
|
5
|
-
export type ResolvedExtractionArtifact = {
|
|
6
|
-
status: "available";
|
|
7
|
-
text: string;
|
|
8
|
-
actualDigest: string;
|
|
9
|
-
} | {
|
|
10
|
-
status: "unavailable";
|
|
11
|
-
code: ArtifactUnavailableCode;
|
|
12
|
-
} | {
|
|
13
|
-
status: "digest-mismatch";
|
|
14
|
-
actualDigest: string;
|
|
15
|
-
};
|
|
4
|
+
export type { ArtifactUnavailableCode, ResolvedExtractionArtifact } from "../extraction-envelope.js";
|
|
16
5
|
export interface ExtractionInspectorEntry {
|
|
17
6
|
/** Result returned by importExtractionEnvelope; includes authoritative ReviewItem identities. */
|
|
18
7
|
importResult: ExtractionEnvelopeImportResult;
|
|
@@ -89,6 +78,29 @@ export interface ExtractionInspectorSource {
|
|
|
89
78
|
artifactText?: string;
|
|
90
79
|
ocrDerived?: true;
|
|
91
80
|
alignment: ExtractionAlignmentState;
|
|
81
|
+
/**
|
|
82
|
+
* Present when the extraction failed, or stopped short without proposing
|
|
83
|
+
* anything: the import's `extraction-failed` / `extraction-incomplete`
|
|
84
|
+
* diagnostic. An empty source with this set is not a run that found nothing.
|
|
85
|
+
*/
|
|
86
|
+
extractionDiagnostic?: Extract<ExtractionEnvelopeImportDiagnostic, {
|
|
87
|
+
kind: "extraction-failed" | "extraction-incomplete";
|
|
88
|
+
}>;
|
|
89
|
+
/**
|
|
90
|
+
* Whether the import checked excerpts against the prepared artifact
|
|
91
|
+
* (`status.provenance`; a record without it is `unverified`). Always set by
|
|
92
|
+
* {@link buildExtractionInspectorModel}; optional for hand-authored models.
|
|
93
|
+
*/
|
|
94
|
+
importProvenance?: "verified" | "unverified";
|
|
95
|
+
/**
|
|
96
|
+
* Proposals the import left out because the prepared text at their span is
|
|
97
|
+
* not their excerpt. They have no candidate row; this is where they are shown.
|
|
98
|
+
*/
|
|
99
|
+
excludedProposals?: Array<{
|
|
100
|
+
proposalIndex: number;
|
|
101
|
+
field: string;
|
|
102
|
+
locator: string;
|
|
103
|
+
}>;
|
|
92
104
|
message: string;
|
|
93
105
|
}
|
|
94
106
|
export interface ExtractionInspectorModel {
|
|
@@ -146,6 +158,12 @@ export interface ExtractionInspectorMountOptions {
|
|
|
146
158
|
* and fails closed if mutable caller data has drifted since import.
|
|
147
159
|
*/
|
|
148
160
|
export declare function buildExtractionInspectorModel(input: ExtractionInspectorInput): BuiltExtractionInspectorModel;
|
|
161
|
+
/**
|
|
162
|
+
* The posture a source is shown with: the extraction's own failure or early
|
|
163
|
+
* stop when there is one, otherwise the artifact alignment. A failed
|
|
164
|
+
* extraction over an aligned artifact must not show the aligned posture.
|
|
165
|
+
*/
|
|
166
|
+
export declare function inspectorSourcePosture(source: Pick<ExtractionInspectorSource, "alignment" | "extractionDiagnostic" | "excludedProposals">): ExtractionAlignmentState | "extraction-failed" | "extraction-incomplete" | "proposals-excluded";
|
|
149
167
|
export declare function filterExtractionInspectorCandidates(model: ExtractionInspectorModel, filters: ExtractionInspectorFilters): ExtractionInspectorCandidate[];
|
|
150
168
|
export declare function exportExtractionInspector(model: ExtractionInspectorModel, options?: ExtractionInspectorExportOptions): string;
|
|
151
169
|
export declare function mountExtractionInspector(container: HTMLElement, model: ExtractionInspectorModel, options?: ExtractionInspectorMountOptions): () => void;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { buildReviewItemsFromExtractionEnvelopeImport, validateExtractionEnvelopeImport } from "../extraction-envelope.js";
|
|
1
|
+
import { assertResolvedExtractionArtifact, buildReviewItemsFromExtractionEnvelopeImport, checkPreparedArtifact, excerptMismatchProposalIndices, proposalSpanMatches, validateExtractionEnvelopeImport } from "../extraction-envelope.js";
|
|
2
2
|
import { canonicalJson } from "./canonical.js";
|
|
3
|
-
import { sha256Hex } from "../sha256.js";
|
|
4
3
|
import { resolvePortablePdfRegion, } from "../pdf-layout.js";
|
|
5
4
|
/**
|
|
6
5
|
* Build a read-only view from one or more results already produced by Survey's
|
|
@@ -19,21 +18,26 @@ export function buildExtractionInspectorModel(input) {
|
|
|
19
18
|
throw new Error("Invalid extraction import result.");
|
|
20
19
|
const record = validateExtractionEnvelopeImport(entry.importResult.record);
|
|
21
20
|
assertImportedResult(entry.importResult, record);
|
|
22
|
-
|
|
21
|
+
assertResolvedExtractionArtifact(entry.artifact);
|
|
23
22
|
const envelope = record.spec.envelope;
|
|
24
23
|
const prepared = envelope.result.preparedArtifact;
|
|
25
24
|
const sourceKey = `${record.metadata.producerNamespace}:${record.metadata.name}:${entryIndex}`;
|
|
26
25
|
if (sourceKeys.has(sourceKey))
|
|
27
26
|
throw new Error("Extraction inspector source identity collision.");
|
|
28
27
|
sourceKeys.add(sourceKey);
|
|
29
|
-
const source = sourceModel(sourceKey, record.metadata.name, prepared, record.status.
|
|
28
|
+
const source = sourceModel(sourceKey, record.metadata.name, prepared, record.status.diagnostics, entry.artifact, envelope.result.ocrDerived, envelope.result.proposals, record.status.provenance ?? "unverified");
|
|
30
29
|
sources.push(source);
|
|
31
30
|
const candidateStart = candidates.length;
|
|
31
|
+
const itemByProposalIndex = new Map();
|
|
32
|
+
for (const item of entry.importResult.reviewItems) {
|
|
33
|
+
for (const proposalIndex of envelopeItemProposalIndices(item))
|
|
34
|
+
itemByProposalIndex.set(proposalIndex, item.metadata.name);
|
|
35
|
+
}
|
|
32
36
|
envelope.result.proposals.forEach((proposal, proposalIndex) => {
|
|
33
|
-
const
|
|
34
|
-
if (!
|
|
37
|
+
const itemName = itemByProposalIndex.get(proposalIndex);
|
|
38
|
+
if (!itemName)
|
|
35
39
|
return; // unresolved imports legitimately produce no ReviewItems
|
|
36
|
-
candidates.push(candidateModel(source,
|
|
40
|
+
candidates.push(candidateModel(source, itemName, proposal, proposalIndex, envelope.result.provider, envelope.result.model, envelope.result.runId, entry.pass, envelope.result.pdfPageOffsets, envelope.result.pdfLayout, envelope.result.ocrDerived));
|
|
37
41
|
});
|
|
38
42
|
if (source.alignment === "excerpt-mismatch") {
|
|
39
43
|
delete source.artifactText;
|
|
@@ -113,25 +117,6 @@ function resolveHighlightElementIds(model) {
|
|
|
113
117
|
}
|
|
114
118
|
return resolved;
|
|
115
119
|
}
|
|
116
|
-
function assertResolvedArtifact(artifact) {
|
|
117
|
-
if (!artifact || typeof artifact !== "object" || Array.isArray(artifact))
|
|
118
|
-
throw new Error("Invalid resolved extraction artifact.");
|
|
119
|
-
const keys = Object.keys(artifact).sort();
|
|
120
|
-
if (artifact.status === "available") {
|
|
121
|
-
if (keys.join(",") !== "actualDigest,status,text" || typeof artifact.text !== "string" || !/^[a-f0-9]{64}$/.test(artifact.actualDigest))
|
|
122
|
-
throw new Error("Invalid available extraction artifact.");
|
|
123
|
-
}
|
|
124
|
-
else if (artifact.status === "digest-mismatch") {
|
|
125
|
-
if (keys.join(",") !== "actualDigest,status" || !/^[a-f0-9]{64}$/.test(artifact.actualDigest))
|
|
126
|
-
throw new Error("Invalid digest-mismatch extraction artifact.");
|
|
127
|
-
}
|
|
128
|
-
else if (artifact.status === "unavailable") {
|
|
129
|
-
if (keys.join(",") !== "code,status" || !["not-found", "storage-error", "access-denied", "invalid-artifact", "unknown"].includes(artifact.code))
|
|
130
|
-
throw new Error("Invalid unavailable extraction artifact.");
|
|
131
|
-
}
|
|
132
|
-
else
|
|
133
|
-
throw new Error("Invalid resolved extraction artifact status.");
|
|
134
|
-
}
|
|
135
120
|
function assertImportedResult(result, record) {
|
|
136
121
|
if (!result || typeof result !== "object" || !result.record || !Array.isArray(result.reviewItems))
|
|
137
122
|
throw new Error("Invalid extraction import result.");
|
|
@@ -141,45 +126,90 @@ function assertImportedResult(result, record) {
|
|
|
141
126
|
if (!record.metadata?.name || !record.metadata.producerNamespace || !record.spec?.envelope?.result || !Array.isArray(record.spec.envelope.result.proposals))
|
|
142
127
|
throw new Error("Malformed extraction import result.");
|
|
143
128
|
const grounded = record.status?.state === "grounded";
|
|
144
|
-
|
|
129
|
+
const proposals = record.spec.envelope.result.proposals;
|
|
130
|
+
const covered = reviewItems.flatMap(envelopeItemProposalIndices).sort((left, right) => left - right);
|
|
131
|
+
const excluded = excerptMismatchProposalIndices(record);
|
|
132
|
+
const expectedCovered = proposals.map((_proposal, index) => index).filter((index) => !excluded.has(index));
|
|
133
|
+
if ((!grounded && reviewItems.length !== 0) || (grounded && canonicalJson(covered) !== canonicalJson(expectedCovered)))
|
|
145
134
|
throw new Error("Extraction import ReviewItems do not match its grounding state.");
|
|
146
135
|
const canonicalItems = buildReviewItemsFromExtractionEnvelopeImport(record);
|
|
147
136
|
if (canonicalJson(reviewItems) !== canonicalJson(canonicalItems))
|
|
148
137
|
throw new Error("Extraction import ReviewItems do not match their canonical identities and bindings.");
|
|
149
|
-
reviewItems.forEach((item,
|
|
150
|
-
const proposal = record.spec.envelope.result.proposals[index];
|
|
138
|
+
reviewItems.forEach((item, itemIndex) => {
|
|
151
139
|
const metadata = item.metadata?.producer?.["survey.kontourai.io/extraction-envelope"];
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
140
|
+
if (item.kind !== "ReviewItem" || !item.metadata.name || item.spec.candidates.length === 0 || metadata?.importName !== record.metadata.name) {
|
|
141
|
+
throw new Error(`Extraction import ReviewItem ${itemIndex} is inconsistent with its validated proposals.`);
|
|
142
|
+
}
|
|
143
|
+
for (const candidate of item.spec.candidates) {
|
|
144
|
+
const binding = candidate.producer?.["survey.kontourai.io/extraction-envelope"];
|
|
145
|
+
const proposal = typeof binding?.proposalIndex === "number" ? proposals[binding.proposalIndex] : undefined;
|
|
146
|
+
if (!proposal || binding?.importName !== record.metadata.name || binding.runId !== record.spec.envelope.result.runId || binding.provider !== record.spec.envelope.result.provider
|
|
147
|
+
|| candidate.extraction.target !== proposal.fieldPath || candidate.locator?.locator !== proposal.provenance.locator || candidate.locator.excerpt !== proposal.provenance.excerpt) {
|
|
148
|
+
throw new Error(`Extraction import ReviewItem ${itemIndex} is inconsistent with its validated proposals.`);
|
|
149
|
+
}
|
|
159
150
|
}
|
|
160
151
|
});
|
|
161
152
|
}
|
|
162
|
-
|
|
153
|
+
/** The proposal indices one imported ReviewItem stands for, as its producer metadata records them. */
|
|
154
|
+
function envelopeItemProposalIndices(item) {
|
|
155
|
+
const metadata = item.metadata?.producer?.["survey.kontourai.io/extraction-envelope"];
|
|
156
|
+
const indices = metadata?.proposalIndices;
|
|
157
|
+
if (!Array.isArray(indices) || indices.length === 0 || !indices.every((index) => Number.isSafeInteger(index) && index >= 0)) {
|
|
158
|
+
throw new Error(`Extraction import ReviewItem ${item.metadata?.name ?? "(unnamed)"} does not record its proposals.`);
|
|
159
|
+
}
|
|
160
|
+
return indices;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The posture a source is shown with: the extraction's own failure or early
|
|
164
|
+
* stop when there is one, otherwise the artifact alignment. A failed
|
|
165
|
+
* extraction over an aligned artifact must not show the aligned posture.
|
|
166
|
+
*/
|
|
167
|
+
export function inspectorSourcePosture(source) {
|
|
168
|
+
// Dropped proposals are a loss of evidence: they lead over an aligned
|
|
169
|
+
// artifact and over an extraction that merely stopped short.
|
|
170
|
+
if (source.alignment === "aligned" && source.excludedProposals?.length && source.extractionDiagnostic?.kind !== "extraction-failed")
|
|
171
|
+
return "proposals-excluded";
|
|
172
|
+
if (source.extractionDiagnostic)
|
|
173
|
+
return source.extractionDiagnostic.kind;
|
|
174
|
+
return source.alignment;
|
|
175
|
+
}
|
|
176
|
+
function sourceModel(key, importName, prepared, diagnostics, artifact, ocrDerived, proposals, importProvenance) {
|
|
163
177
|
let alignment;
|
|
164
178
|
let message;
|
|
165
|
-
|
|
179
|
+
const artifactUnresolved = diagnostics.some((diagnostic) => diagnostic.kind === "artifact-unavailable" || diagnostic.kind === "digest-mismatch");
|
|
180
|
+
const excludedProposals = diagnostics.flatMap((diagnostic) => diagnostic.kind === "excerpt-mismatch"
|
|
181
|
+
? [{ proposalIndex: diagnostic.proposalIndex, field: proposals[diagnostic.proposalIndex]?.fieldPath ?? "", locator: diagnostic.locator }]
|
|
182
|
+
: []);
|
|
183
|
+
const extractionDiagnostic = diagnostics.find((diagnostic) => diagnostic.kind === "extraction-failed" || diagnostic.kind === "extraction-incomplete");
|
|
184
|
+
const check = checkPreparedArtifact(prepared, artifact);
|
|
185
|
+
if (artifactUnresolved || check.status === "unavailable") {
|
|
166
186
|
alignment = "artifact-unavailable";
|
|
167
|
-
message = `Prepared artifact unavailable (${
|
|
187
|
+
message = `Prepared artifact unavailable (${check.status === "unavailable" ? check.code : "invalid-artifact"}). Candidates are not grounded.`;
|
|
168
188
|
}
|
|
169
|
-
else if (
|
|
170
|
-
|| sha256Hex(artifact.text) !== artifact.actualDigest) {
|
|
189
|
+
else if (check.status === "digest-mismatch") {
|
|
171
190
|
alignment = "digest-mismatch";
|
|
172
191
|
message = "Prepared artifact digest does not match the extraction artifact. Candidates are not grounded.";
|
|
173
192
|
}
|
|
174
|
-
else if (
|
|
193
|
+
else if (check.status === "wrong-length") {
|
|
175
194
|
alignment = "artifact-unavailable";
|
|
176
195
|
message = "Prepared artifact content has the wrong length. Candidates are not grounded.";
|
|
177
196
|
}
|
|
178
197
|
else {
|
|
179
198
|
alignment = "aligned";
|
|
180
|
-
message =
|
|
199
|
+
message = extractionDiagnostic || excludedProposals.length
|
|
200
|
+
? "Prepared artifact identity verified."
|
|
201
|
+
: `Prepared artifact identity verified. Exact source spans are available.${ocrDerived ? " Prepared text is OCR-derived." : ""}`;
|
|
202
|
+
}
|
|
203
|
+
if (excludedProposals.length) {
|
|
204
|
+
const named = excludedProposals.map((p) => `#${p.proposalIndex} ${p.field} (${p.locator})`).join(", ");
|
|
205
|
+
message = `${excludedProposals.length} proposal${excludedProposals.length === 1 ? " was" : "s were"} excluded at import because the prepared text at the span is not the excerpt: ${named}. ${message}`;
|
|
181
206
|
}
|
|
182
|
-
|
|
207
|
+
message = `${message} ${importProvenance === "verified" ? "Excerpts were checked against the prepared artifact at import." : "Excerpts were not checked against the prepared artifact at import."}`;
|
|
208
|
+
// The extraction's own failure leads: an aligned artifact with no candidates
|
|
209
|
+
// must not read as a complete run that found nothing.
|
|
210
|
+
if (extractionDiagnostic)
|
|
211
|
+
message = `${extractionDiagnostic.message} ${message}`;
|
|
212
|
+
return { key, importName, importProvenance, ...(excludedProposals.length ? { excludedProposals } : {}), ...(extractionDiagnostic ? { extractionDiagnostic } : {}), ...(prepared?.ref ? { artifactRef: prepared.ref } : {}), ...(prepared?.digest ? { expectedDigest: prepared.digest } : {}), ...("actualDigest" in artifact ? { actualDigest: artifact.actualDigest } : {}), ...(alignment === "aligned" && artifact.status === "available" ? { artifactText: artifact.text } : {}), ...(ocrDerived ? { ocrDerived: true } : {}), alignment, message };
|
|
183
213
|
}
|
|
184
214
|
function candidateModel(source, reviewItemName, proposal, index, provider, model, attempt, pass, pdfPageOffsets, pdfLayout, ocrDerived) {
|
|
185
215
|
const match = /^chars:(\d+)-(\d+)$/.exec(proposal.provenance.locator);
|
|
@@ -188,10 +218,10 @@ function candidateModel(source, reviewItemName, proposal, index, provider, model
|
|
|
188
218
|
const start = Number(match[1]), end = Number(match[2]);
|
|
189
219
|
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || end < start)
|
|
190
220
|
throw new Error(`Extraction proposal ${index} has an invalid text span.`);
|
|
191
|
-
const alignment = source.alignment === "aligned" && source.artifactText
|
|
221
|
+
const alignment = source.alignment === "aligned" && !proposalSpanMatches(source.artifactText, proposal) ? "excerpt-mismatch" : source.alignment;
|
|
192
222
|
if (alignment === "excerpt-mismatch") {
|
|
193
223
|
source.alignment = alignment;
|
|
194
|
-
source.message =
|
|
224
|
+
source.message = `One or more source spans do not match their recorded excerpts. Affected candidates are not grounded.${source.importProvenance === "unverified" ? " Excerpts were not checked against the prepared artifact at import." : ""}`;
|
|
195
225
|
}
|
|
196
226
|
let pdfRegion = pdfLayout ? resolvePortablePdfRegion(pdfLayout, proposal.provenance.locator) : undefined;
|
|
197
227
|
const page = resolvePdfPage(pdfPageOffsets, start);
|
|
@@ -270,7 +300,7 @@ export function mountExtractionInspector(container, model, options = {}) {
|
|
|
270
300
|
next.hidden = pageCount === 1;
|
|
271
301
|
previous.disabled = page === 0;
|
|
272
302
|
next.disabled = page >= pageCount - 1;
|
|
273
|
-
postures.innerHTML = model.sources.map(s => `<div class="inspector-posture ${
|
|
303
|
+
postures.innerHTML = model.sources.map(s => { const posture = inspectorSourcePosture(s); const classes = [...new Set([s.alignment, s.extractionDiagnostic?.kind, s.excludedProposals?.length ? "proposals-excluded" : undefined, posture].filter(Boolean))].join(" "); return `<div class="inspector-posture ${classes}" role="status" data-posture="${escapeHtml(posture)}"><strong>${escapeHtml(s.importName)}: ${escapeHtml(posture)}</strong><span>${escapeHtml(s.message)}</span></div>`; }).join("");
|
|
274
304
|
sourcesRoot.innerHTML = model.sources.map(s => { const anchored = model.candidates.filter(c => c.sourceKey === s.key); const marked = visible.filter(c => c.sourceKey === s.key); return `<div class="inspector-source" aria-label="Prepared source for ${escapeHtml(s.importName)}"><h3>${escapeHtml(s.importName)}</h3><pre tabindex="0">${s.artifactText === undefined ? `${anchored.map(c => anchorHtml(c, highlightIdFor(c))).join("")}<span class="source-unavailable">${escapeHtml(s.message)}</span>` : renderSource(s.artifactText, anchored, marked, highlightIdFor)}</pre></div>`; }).join("");
|
|
275
305
|
};
|
|
276
306
|
root.querySelectorAll("select").forEach(select => select.addEventListener("change", event => { event.stopPropagation(); const key = select.dataset.filter; if (select.value)
|
|
@@ -229,7 +229,7 @@ export function validateReviewQueueAgainstExtractionImport(items, importResult)
|
|
|
229
229
|
if (record.status.state !== "grounded") {
|
|
230
230
|
return [{
|
|
231
231
|
code: "import-not-grounded",
|
|
232
|
-
message: `Extraction import ${record.metadata.name} is ${record.status.state}, not grounded; it cannot attest a review queue.`,
|
|
232
|
+
message: `Extraction import ${record.metadata.name} is ${record.status.state}, not grounded (${record.status.diagnostics.map((diagnostic) => diagnostic.message).join(" ")}); it cannot attest a review queue.`,
|
|
233
233
|
}];
|
|
234
234
|
}
|
|
235
235
|
if (items.length === 0) {
|
|
@@ -50,6 +50,25 @@ export interface ReviewItemPresentation {
|
|
|
50
50
|
readonly reviewItemLink?: ReviewPresentationLink;
|
|
51
51
|
readonly traceRefs: readonly ReviewTraceRef[];
|
|
52
52
|
readonly candidates: readonly ReviewCandidatePresentation[];
|
|
53
|
+
/**
|
|
54
|
+
* Proposals for this claim that an envelope import left out because the
|
|
55
|
+
* source text at their cited span is not their excerpt. Unverifiable, not
|
|
56
|
+
* disproven: every decision surface shows them. Empty for other items.
|
|
57
|
+
*/
|
|
58
|
+
readonly excludedProposals: readonly ExcludedProposalPresentation[];
|
|
59
|
+
/**
|
|
60
|
+
* For an envelope-imported item, whether its import checked excerpts
|
|
61
|
+
* against the prepared artifact. Absent for other items, and for an item
|
|
62
|
+
* whose envelope binding is missing, which cannot claim either.
|
|
63
|
+
*/
|
|
64
|
+
readonly excerptVerification?: "verified" | "unverified";
|
|
65
|
+
}
|
|
66
|
+
export interface ExcludedProposalPresentation {
|
|
67
|
+
readonly proposalIndex: number;
|
|
68
|
+
readonly value: unknown;
|
|
69
|
+
readonly valueText: string;
|
|
70
|
+
readonly locator: string;
|
|
71
|
+
readonly excerpt: string;
|
|
53
72
|
}
|
|
54
73
|
export interface ReviewResultPresentation {
|
|
55
74
|
readonly result: ReviewWorkbenchResult;
|
|
@@ -57,7 +76,8 @@ export interface ReviewResultPresentation {
|
|
|
57
76
|
readonly target: string;
|
|
58
77
|
readonly targetLabel: string;
|
|
59
78
|
readonly decisionLabel: string;
|
|
60
|
-
|
|
79
|
+
/** Absent when the decision selects no candidate (reject-all or could-not-confirm on a conflict). */
|
|
80
|
+
readonly selectedValueText?: string;
|
|
61
81
|
readonly applyMeaning: string;
|
|
62
82
|
readonly reviewItemLink?: ReviewPresentationLink;
|
|
63
83
|
readonly traceRefs: readonly ReviewTraceRef[];
|
|
@@ -105,6 +125,11 @@ export interface InterpretationReadingPresentation {
|
|
|
105
125
|
* authored record under a label nothing derived.
|
|
106
126
|
*/
|
|
107
127
|
export declare function buildInterpretationReadingPresentation(source: InterpretationReadingSource): InterpretationReadingPresentation;
|
|
128
|
+
/**
|
|
129
|
+
* One sentence naming every excluded proposal, for surfaces that speak in
|
|
130
|
+
* text (the MCP item, the recorded decision prompt). Undefined when none.
|
|
131
|
+
*/
|
|
132
|
+
export declare function excludedProposalsSentence(excluded: readonly ExcludedProposalPresentation[]): string | undefined;
|
|
108
133
|
export declare function buildReviewItemPresentation(item: ReviewItem, adapter?: ReviewPresentationAdapter): ReviewItemPresentation;
|
|
109
134
|
export declare function buildReviewCandidatePresentation(item: ReviewItem, candidate: ReviewCandidate, adapter?: ReviewPresentationAdapter, targetLabel?: string): ReviewCandidatePresentation;
|
|
110
135
|
export declare function buildReviewResultPresentation(result: ReviewWorkbenchResult, item: ReviewItem | undefined, adapter?: ReviewPresentationAdapter): ReviewResultPresentation;
|
|
@@ -49,6 +49,50 @@ export function buildInterpretationReadingPresentation(source) {
|
|
|
49
49
|
provenanceLabel: "Authored judgment",
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
+
const EXTRACTION_ENVELOPE_PRODUCER = "survey.kontourai.io/extraction-envelope";
|
|
53
|
+
/**
|
|
54
|
+
* The envelope-import binding an item carries, or undefined when it has none.
|
|
55
|
+
* A binding names its import and proposals, and every candidate points back
|
|
56
|
+
* at the same import; anything less cannot vouch for what the import checked.
|
|
57
|
+
*/
|
|
58
|
+
function extractionEnvelopeBinding(item) {
|
|
59
|
+
const metadata = item.metadata?.producer?.[EXTRACTION_ENVELOPE_PRODUCER];
|
|
60
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
61
|
+
return undefined;
|
|
62
|
+
const binding = metadata;
|
|
63
|
+
if (typeof binding.importName !== "string" || !binding.importName || !Array.isArray(binding.proposalIndices) || binding.proposalIndices.length === 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
const bound = item.spec.candidates.length > 0 && item.spec.candidates.every((candidate) => {
|
|
66
|
+
const own = candidate.producer?.[EXTRACTION_ENVELOPE_PRODUCER];
|
|
67
|
+
return own?.importName === binding.importName;
|
|
68
|
+
});
|
|
69
|
+
return bound ? binding : undefined;
|
|
70
|
+
}
|
|
71
|
+
/** The well-formed `excludedProposals` entries of a bound envelope item; malformed entries are ignored, never rendered. */
|
|
72
|
+
function excludedProposalsOf(item, binding, adapter) {
|
|
73
|
+
if (!binding || !Array.isArray(binding.excludedProposals))
|
|
74
|
+
return [];
|
|
75
|
+
return binding.excludedProposals.flatMap((entry) => {
|
|
76
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry))
|
|
77
|
+
return [];
|
|
78
|
+
const e = entry;
|
|
79
|
+
if (!Number.isSafeInteger(e.proposalIndex) || typeof e.locator !== "string" || typeof e.excerpt !== "string" || !("value" in e))
|
|
80
|
+
return [];
|
|
81
|
+
const proposed = item.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
82
|
+
const valueText = (proposed ? adapter.summarizeValue?.(e.value, { item, candidate: proposed, value: e.value }) : undefined) ?? formatValue(e.value);
|
|
83
|
+
return [{ proposalIndex: e.proposalIndex, value: e.value, valueText, locator: e.locator, excerpt: e.excerpt }];
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* One sentence naming every excluded proposal, for surfaces that speak in
|
|
88
|
+
* text (the MCP item, the recorded decision prompt). Undefined when none.
|
|
89
|
+
*/
|
|
90
|
+
export function excludedProposalsSentence(excluded) {
|
|
91
|
+
if (excluded.length === 0)
|
|
92
|
+
return undefined;
|
|
93
|
+
const named = excluded.map((entry) => `${entry.valueText} (proposal ${entry.proposalIndex}, ${entry.locator})`).join(", ");
|
|
94
|
+
return `${excluded.length === 1 ? "Another proposed value was" : `${excluded.length} other proposed values were`} excluded at import because the source text at the cited span is not the excerpt: ${named}. Unverifiable is not disproven.`;
|
|
95
|
+
}
|
|
52
96
|
export function buildReviewItemPresentation(item, adapter = {}) {
|
|
53
97
|
const context = { item };
|
|
54
98
|
const targetLabel = adapter.labelForTarget?.(item.spec.target, context) ?? humanizeIdentifier(item.spec.target);
|
|
@@ -61,6 +105,14 @@ export function buildReviewItemPresentation(item, adapter = {}) {
|
|
|
61
105
|
reviewItemLink: adapter.linkForReviewItem?.(item, context),
|
|
62
106
|
traceRefs: traceRefsForReviewItem(item, adapter),
|
|
63
107
|
candidates: item.spec.candidates.map((candidate) => buildReviewCandidatePresentation(item, candidate, adapter, targetLabel)),
|
|
108
|
+
...extractionPresentation(item, adapter),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function extractionPresentation(item, adapter) {
|
|
112
|
+
const binding = extractionEnvelopeBinding(item);
|
|
113
|
+
return {
|
|
114
|
+
excludedProposals: excludedProposalsOf(item, binding, adapter),
|
|
115
|
+
...(binding ? { excerptVerification: binding.excerptVerification === "verified" ? "verified" : "unverified" } : {}),
|
|
64
116
|
};
|
|
65
117
|
}
|
|
66
118
|
export function buildReviewCandidatePresentation(item, candidate, adapter = {}, targetLabel = adapter.labelForTarget?.(item.spec.target, { item }) ?? humanizeIdentifier(item.spec.target)) {
|
|
@@ -85,6 +137,28 @@ export function buildReviewResultPresentation(result, item, adapter = {}) {
|
|
|
85
137
|
? adapter.labelForTarget?.(target, itemContext) ?? humanizeIdentifier(target)
|
|
86
138
|
: humanizeIdentifier(target);
|
|
87
139
|
const selectedCandidate = item ? selectedCandidateForResult(item, result) : undefined;
|
|
140
|
+
// A decision that selects no candidate presents no selected value and no
|
|
141
|
+
// selected trace; it names every candidate instead.
|
|
142
|
+
if (result.selectedCandidateId === undefined) {
|
|
143
|
+
const rejected = result.decision === "reject-proposed";
|
|
144
|
+
return {
|
|
145
|
+
result,
|
|
146
|
+
item,
|
|
147
|
+
target,
|
|
148
|
+
targetLabel,
|
|
149
|
+
decisionLabel: humanizeIdentifier(result.decision),
|
|
150
|
+
applyMeaning: rejected
|
|
151
|
+
? "Saved decision rejects every proposed value; none is applied"
|
|
152
|
+
: "Saved decision records that no proposed value could be confirmed; none is applied",
|
|
153
|
+
reviewItemLink: item && itemContext ? adapter.linkForReviewItem?.(item, itemContext) : undefined,
|
|
154
|
+
traceRefs: [
|
|
155
|
+
{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item", context: undefined },
|
|
156
|
+
...result.unselectedCandidates.map((candidate) => ({
|
|
157
|
+
label: rejected ? "Rejected candidate" : "Unconfirmed candidate", value: candidate.id, kind: "candidate", context: candidate,
|
|
158
|
+
})),
|
|
159
|
+
].flatMap(({ context, ...ref }) => (item ? withTraceLinks([ref], { item, candidate: context }, adapter) : [ref])),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
88
162
|
return {
|
|
89
163
|
result,
|
|
90
164
|
item,
|
|
@@ -168,7 +242,7 @@ function traceRefsForCandidate(item, candidate, adapter) {
|
|
|
168
242
|
function traceRefsForResult(item, result, selectedCandidate, adapter) {
|
|
169
243
|
return withTraceLinks([
|
|
170
244
|
{ label: "Survey ReviewItem", value: result.reviewItemName, kind: "review-item" },
|
|
171
|
-
{ label: "Selected candidate", value: result.selectedCandidateId, kind: "candidate" },
|
|
245
|
+
{ label: "Selected candidate", value: result.selectedCandidateId ?? "none", kind: "candidate" },
|
|
172
246
|
{
|
|
173
247
|
label: "Selected claim",
|
|
174
248
|
value: selectedCandidate?.claimTarget.claimId ?? "not provided",
|