@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
|
@@ -94,15 +94,29 @@ export declare function keepActionDecision(item: ReviewItem, flaggedWrong: boole
|
|
|
94
94
|
/**
|
|
95
95
|
* The candidate a workbench decision applies to.
|
|
96
96
|
*
|
|
97
|
-
* Selection is by role,
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* leaves here. Guarding the render path alone let the workbench emit an
|
|
101
|
-
* undecidable decision through the export path and then present a different
|
|
97
|
+
* Selection is by role, so the role has to name exactly one candidate before a
|
|
98
|
+
* decision may trust it. Guarding the render path alone let the workbench emit
|
|
99
|
+
* an undecidable decision through the export path and then present a different
|
|
102
100
|
* candidate's value against it; this is the shared selector all of those go
|
|
103
101
|
* through, which is why the check belongs here rather than at each of them.
|
|
102
|
+
*
|
|
103
|
+
* A decision that would make one of several candidates in its role the trusted
|
|
104
|
+
* value is refused: picking the first would settle the conflict for the
|
|
105
|
+
* reviewer without showing it. A value-neutral decision (reject, could not
|
|
106
|
+
* confirm) on such an item selects none of them. It still returns the first
|
|
107
|
+
* candidate so in-process callers that need one candidate (rendering, the
|
|
108
|
+
* in-memory `ReviewWorkbenchResult`) have one, but that anchor is not recorded:
|
|
109
|
+
* {@link decisionCandidateId} is `undefined` for it, so the decision, its
|
|
110
|
+
* session events and the canonical projection name no candidate.
|
|
104
111
|
*/
|
|
105
112
|
export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision): ReviewCandidate;
|
|
113
|
+
/** Whether a decision on this item selects no candidate at all (see {@link candidateForDecision}). */
|
|
114
|
+
export declare function decisionSelectsNoCandidate(item: ReviewItem, decision: ReviewWorkbenchDecision): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* The candidate id a decision records: the selected candidate's, or
|
|
117
|
+
* `undefined` when the decision selects no candidate.
|
|
118
|
+
*/
|
|
119
|
+
export declare function decisionCandidateId(item: ReviewItem, decision: ReviewWorkbenchDecision): string | undefined;
|
|
106
120
|
/**
|
|
107
121
|
* The value that should actually be applied for a decision: the reviewer's inline
|
|
108
122
|
* edit when one was made for an accept-proposed decision, otherwise the selected
|
|
@@ -143,32 +143,65 @@ export function reviewSessionSummary(session) {
|
|
|
143
143
|
* would invent a prior value, with provenance, that the source never had.
|
|
144
144
|
*/
|
|
145
145
|
export function keepActionDecision(item, flaggedWrong) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
// Keeping the current value is recordable only when exactly one candidate is
|
|
147
|
+
// current (see candidateForDecision); rejecting the proposed values is
|
|
148
|
+
// recordable however many there are.
|
|
149
|
+
const count = (role) => item.spec.candidates.filter((candidate) => candidate.role === role).length;
|
|
150
|
+
if (flaggedWrong || count("current") === 0) {
|
|
151
|
+
return count("proposed") > 0 ? "reject-proposed" : undefined;
|
|
149
152
|
}
|
|
150
|
-
return "keep-current";
|
|
153
|
+
return count("current") === 1 ? "keep-current" : undefined;
|
|
151
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Decisions that make no candidate the trusted value: rejecting the proposed
|
|
157
|
+
* values and ending the round as could-not-confirm. On an item whose role holds
|
|
158
|
+
* several candidates (conflicting values for one claim) they select none of them.
|
|
159
|
+
*/
|
|
160
|
+
const VALUE_NEUTRAL_DECISIONS = new Set(["reject-proposed", "could-not-confirm"]);
|
|
152
161
|
/**
|
|
153
162
|
* The candidate a workbench decision applies to.
|
|
154
163
|
*
|
|
155
|
-
* Selection is by role,
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
* leaves here. Guarding the render path alone let the workbench emit an
|
|
159
|
-
* undecidable decision through the export path and then present a different
|
|
164
|
+
* Selection is by role, so the role has to name exactly one candidate before a
|
|
165
|
+
* decision may trust it. Guarding the render path alone let the workbench emit
|
|
166
|
+
* an undecidable decision through the export path and then present a different
|
|
160
167
|
* candidate's value against it; this is the shared selector all of those go
|
|
161
168
|
* through, which is why the check belongs here rather than at each of them.
|
|
169
|
+
*
|
|
170
|
+
* A decision that would make one of several candidates in its role the trusted
|
|
171
|
+
* value is refused: picking the first would settle the conflict for the
|
|
172
|
+
* reviewer without showing it. A value-neutral decision (reject, could not
|
|
173
|
+
* confirm) on such an item selects none of them. It still returns the first
|
|
174
|
+
* candidate so in-process callers that need one candidate (rendering, the
|
|
175
|
+
* in-memory `ReviewWorkbenchResult`) have one, but that anchor is not recorded:
|
|
176
|
+
* {@link decisionCandidateId} is `undefined` for it, so the decision, its
|
|
177
|
+
* session events and the canonical projection name no candidate.
|
|
162
178
|
*/
|
|
163
179
|
export function candidateForDecision(item, decision) {
|
|
164
180
|
const definition = workbenchDecisionDefinitions[decision];
|
|
165
|
-
const
|
|
181
|
+
const matches = item.spec.candidates.filter((entry) => entry.role === definition.candidateRole);
|
|
182
|
+
const candidate = matches[0];
|
|
166
183
|
if (!candidate) {
|
|
167
184
|
throw new Error(`ReviewItem ${item.metadata.name} has no ${definition.candidateRole} candidate.`);
|
|
168
185
|
}
|
|
186
|
+
if (matches.length > 1 && !VALUE_NEUTRAL_DECISIONS.has(decision)) {
|
|
187
|
+
throw new Error(`ReviewItem ${item.metadata.name} has ${matches.length} ${definition.candidateRole} candidates; the ${decision} decision cannot choose between them.`);
|
|
188
|
+
}
|
|
169
189
|
assertSoleCandidateId(item, candidate.id);
|
|
170
190
|
return candidate;
|
|
171
191
|
}
|
|
192
|
+
/** Whether a decision on this item selects no candidate at all (see {@link candidateForDecision}). */
|
|
193
|
+
export function decisionSelectsNoCandidate(item, decision) {
|
|
194
|
+
const role = workbenchDecisionDefinitions[decision].candidateRole;
|
|
195
|
+
return VALUE_NEUTRAL_DECISIONS.has(decision) && item.spec.candidates.filter((entry) => entry.role === role).length > 1;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* The candidate id a decision records: the selected candidate's, or
|
|
199
|
+
* `undefined` when the decision selects no candidate.
|
|
200
|
+
*/
|
|
201
|
+
export function decisionCandidateId(item, decision) {
|
|
202
|
+
const candidate = candidateForDecision(item, decision);
|
|
203
|
+
return decisionSelectsNoCandidate(item, decision) ? undefined : candidate.id;
|
|
204
|
+
}
|
|
172
205
|
/**
|
|
173
206
|
* The value that should actually be applied for a decision: the reviewer's inline
|
|
174
207
|
* edit when one was made for an accept-proposed decision, otherwise the selected
|
|
@@ -254,7 +287,7 @@ export function buildReviewSessionEvents(session, sessionName = defaultReviewSes
|
|
|
254
287
|
if (decision === "could-not-confirm" && !note?.trim()) {
|
|
255
288
|
throw new Error(`ReviewItem ${item.metadata.name} could not confirm requires a non-empty reason.`);
|
|
256
289
|
}
|
|
257
|
-
const
|
|
290
|
+
const candidateId = decisionCandidateId(item, decision);
|
|
258
291
|
const definition = workbenchDecisionDefinitions[decision];
|
|
259
292
|
const reviewDecisionName = `${item.metadata.name}-${decision}`;
|
|
260
293
|
// Carry the reviewer's inline edit in the event itself (accept-proposed
|
|
@@ -284,7 +317,7 @@ export function buildReviewSessionEvents(session, sessionName = defaultReviewSes
|
|
|
284
317
|
occurredAt: session.reviewedAt,
|
|
285
318
|
reviewItemName: item.metadata.name,
|
|
286
319
|
reviewDecisionName,
|
|
287
|
-
candidateId:
|
|
320
|
+
...(candidateId !== undefined ? { candidateId } : {}),
|
|
288
321
|
status: definition.status,
|
|
289
322
|
...(decision === "could-not-confirm"
|
|
290
323
|
? {
|
|
@@ -302,7 +335,7 @@ export function buildReviewSessionEvents(session, sessionName = defaultReviewSes
|
|
|
302
335
|
occurredAt: session.reviewedAt,
|
|
303
336
|
reviewItemName: item.metadata.name,
|
|
304
337
|
reviewDecisionName,
|
|
305
|
-
candidateId:
|
|
338
|
+
...(candidateId !== undefined ? { candidateId } : {}),
|
|
306
339
|
status: definition.status,
|
|
307
340
|
rationale: note,
|
|
308
341
|
...(decision === "could-not-confirm"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { candidateForDecision, isClearedWorkbenchDecisionEvent, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
1
|
+
import { candidateForDecision, decisionSelectsNoCandidate, isClearedWorkbenchDecisionEvent, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
2
2
|
import { checkEditedValueForItem } from "./edited-value.js";
|
|
3
3
|
export function validateReviewSessionEventsForSnapshot(snapshot, events) {
|
|
4
4
|
const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
|
|
@@ -56,17 +56,19 @@ export function validateReviewSessionEventsForSnapshot(snapshot, events) {
|
|
|
56
56
|
else if (itemName && itemsByName.has(itemName)) {
|
|
57
57
|
const item = itemsByName.get(itemName);
|
|
58
58
|
const expectedCandidate = item ? candidateForDecision(item, decision) : undefined;
|
|
59
|
+
// A decision that selects no candidate must reference none.
|
|
60
|
+
const expectedCandidateId = item && !decisionSelectsNoCandidate(item, decision) ? expectedCandidate?.id : undefined;
|
|
59
61
|
const expectedStatus = workbenchDecisionDefinitions[decision].status;
|
|
60
62
|
const referencedCandidateExists = event.spec.candidateId
|
|
61
63
|
? item?.spec.candidates.some((candidate) => candidate.id === event.spec.candidateId)
|
|
62
64
|
: false;
|
|
63
|
-
if (expectedCandidate && (!event.spec.candidateId || referencedCandidateExists) && event.spec.candidateId !==
|
|
65
|
+
if (expectedCandidate && (!event.spec.candidateId || referencedCandidateExists) && event.spec.candidateId !== expectedCandidateId) {
|
|
64
66
|
issues.push({
|
|
65
67
|
...eventRef,
|
|
66
68
|
code: "decision-candidate-mismatch",
|
|
67
69
|
reviewItemName: itemName,
|
|
68
70
|
candidateId: event.spec.candidateId,
|
|
69
|
-
message: `ReviewSessionEvent ${event.metadata.name} decision ${decision} expects candidate ${
|
|
71
|
+
message: `ReviewSessionEvent ${event.metadata.name} decision ${decision} expects ${expectedCandidateId ? `candidate ${expectedCandidateId}` : "no candidate"}, but references ${event.spec.candidateId ?? "no candidate"}.`,
|
|
70
72
|
});
|
|
71
73
|
}
|
|
72
74
|
if (event.spec.status !== expectedStatus) {
|
|
@@ -246,7 +246,10 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
246
246
|
.survey-workbench-embed .inspector-heading, .survey-workbench-embed .inspector-layout{ display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
247
247
|
.survey-workbench-embed .inspector-heading h2{ margin: 0; }
|
|
248
248
|
.survey-workbench-embed .inspector-posture{ display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
249
|
-
|
|
249
|
+
/* Caution first: an artifact failure on the same source must win the tie and stay negative. */
|
|
250
|
+
.survey-workbench-embed .inspector-posture.extraction-incomplete{ background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
251
|
+
.survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch, .survey-workbench-embed .inspector-posture.proposals-excluded{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
252
|
+
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
250
253
|
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
251
254
|
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
252
255
|
.survey-workbench-embed .inspector-filters select, .survey-workbench-embed .inspector-filters input{ color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -832,6 +835,12 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
832
835
|
font-size: 12px;
|
|
833
836
|
}
|
|
834
837
|
|
|
838
|
+
.survey-workbench-embed .excerpt-verification{
|
|
839
|
+
margin: 0;
|
|
840
|
+
font-size: 12px;
|
|
841
|
+
color: var(--k-muted);
|
|
842
|
+
}
|
|
843
|
+
|
|
835
844
|
/* decisions */
|
|
836
845
|
|
|
837
846
|
/* The decision row is the point of the card, so it sits on its own line,
|
|
@@ -243,7 +243,10 @@
|
|
|
243
243
|
.survey-workbench-embed .inspector-heading, .survey-workbench-embed .inspector-layout{ display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
244
244
|
.survey-workbench-embed .inspector-heading h2{ margin: 0; }
|
|
245
245
|
.survey-workbench-embed .inspector-posture{ display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
246
|
-
|
|
246
|
+
/* Caution first: an artifact failure on the same source must win the tie and stay negative. */
|
|
247
|
+
.survey-workbench-embed .inspector-posture.extraction-incomplete{ background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
248
|
+
.survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch, .survey-workbench-embed .inspector-posture.proposals-excluded{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
249
|
+
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
247
250
|
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
248
251
|
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
249
252
|
.survey-workbench-embed .inspector-filters select, .survey-workbench-embed .inspector-filters input{ color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -829,6 +832,12 @@
|
|
|
829
832
|
font-size: 12px;
|
|
830
833
|
}
|
|
831
834
|
|
|
835
|
+
.survey-workbench-embed .excerpt-verification{
|
|
836
|
+
margin: 0;
|
|
837
|
+
font-size: 12px;
|
|
838
|
+
color: var(--k-muted);
|
|
839
|
+
}
|
|
840
|
+
|
|
832
841
|
/* decisions */
|
|
833
842
|
|
|
834
843
|
/* The decision row is the point of the card, so it sits on its own line,
|
|
@@ -5,7 +5,7 @@ import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type Review
|
|
|
5
5
|
export { reviewAuditRowKeys, type ReviewAuditRowKey } from "./audit-rows.js";
|
|
6
6
|
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, type BindReviewQueueOptions, type ReviewQueueBinding, type ReviewQueueBindingIssue, type ReviewQueueBindingIssueCode, type ReviewQueueExtractionIssue, type ReviewQueueExtractionIssueCode, type ValidateReviewQueueBindingOptions, } from "./queue-binding.js";
|
|
7
7
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, type ExtractionAlignmentState, type ArtifactUnavailableCode, type BuiltExtractionInspectorCandidate, type BuiltExtractionInspectorModel, type ExtractionInspectorCandidate, type ExtractionInspectorEntry, type ExtractionInspectorExportOptions, type ExtractionInspectorFilters, type ExtractionInspectorInput, type ExtractionInspectorMountOptions, type ExtractionInspectorModel, type ExtractionInspectorSource, type ResolvedExtractionArtifact, } from "./extraction-inspector.js";
|
|
8
|
-
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
8
|
+
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
9
9
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
|
|
10
10
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
11
11
|
export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, type ReviewSessionReplayWarning, } from "./review-session-replay.js";
|
|
@@ -110,24 +110,31 @@ export interface ReviewQueueWindow {
|
|
|
110
110
|
export interface BrowserReviewWorkbenchConfig {
|
|
111
111
|
readonly startState?: ReviewQueueSessionState | ReviewWorkbenchState;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* The in-process outcome of one workbench decision, derived from the session.
|
|
115
|
+
* For a decision that selects no candidate ({@link decisionSelectsNoCandidate}:
|
|
116
|
+
* reject-all or could-not-confirm on a conflict), every `selected*` and
|
|
117
|
+
* `effective*` field is absent, `unselectedCandidates` holds every candidate,
|
|
118
|
+
* and `reviewDecision.spec.candidateId` is absent.
|
|
119
|
+
*/
|
|
113
120
|
export interface ReviewWorkbenchResult {
|
|
114
121
|
readonly reviewItemName: string;
|
|
115
122
|
readonly decision: ReviewWorkbenchDecision;
|
|
116
|
-
readonly selectedCandidate
|
|
117
|
-
readonly selectedCandidateId
|
|
123
|
+
readonly selectedCandidate?: ReviewCandidate;
|
|
124
|
+
readonly selectedCandidateId?: string;
|
|
118
125
|
readonly selectedCandidateRole?: ReviewCandidate["role"];
|
|
119
126
|
/** The selected candidate's original value, unaffected by any reviewer edit. Used
|
|
120
127
|
* for candidate-identity matching (see `matchingSelectedCandidate`); consumers who
|
|
121
128
|
* want the reviewer's edited value should read `effectiveValue` instead. */
|
|
122
|
-
readonly selectedValue
|
|
123
|
-
readonly selectedDisplayValue
|
|
129
|
+
readonly selectedValue?: unknown;
|
|
130
|
+
readonly selectedDisplayValue?: string;
|
|
124
131
|
/** Reviewer-edited override captured for an accept-proposed decision, if the
|
|
125
132
|
* reviewer changed the proposed value before applying it. Additive/optional. */
|
|
126
133
|
readonly editedValue?: unknown;
|
|
127
134
|
/** The value that should actually be applied: `editedValue` when present (and the
|
|
128
135
|
* decision selects the proposed candidate), otherwise `selectedValue`. */
|
|
129
|
-
readonly effectiveValue
|
|
130
|
-
readonly effectiveDisplayValue
|
|
136
|
+
readonly effectiveValue?: unknown;
|
|
137
|
+
readonly effectiveDisplayValue?: string;
|
|
131
138
|
readonly unselectedCandidates: readonly ReviewCandidate[];
|
|
132
139
|
readonly reviewDecision: ReviewDecision;
|
|
133
140
|
readonly status: ReviewDecision["spec"]["status"];
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical.js";
|
|
2
2
|
import { assertReviewResolutionConsistency } from "../producer-discipline.js";
|
|
3
|
-
import { candidateForDecision, keepActionDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
3
|
+
import { candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
4
4
|
import { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
5
5
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
6
6
|
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
7
7
|
import { findSoleCandidateById, reviewResourceApiVersion, } from "../review-resource.js";
|
|
8
8
|
import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
|
|
9
|
-
import { humanizeIdentifier } from "./review-presentation.js";
|
|
9
|
+
import { excludedProposalsSentence, humanizeIdentifier } from "./review-presentation.js";
|
|
10
10
|
import { editedValueFromEditorText, isIsoCalendarDate, parsePlainDecimal } from "./edited-value.js";
|
|
11
11
|
import { createAuditFactTrace, } from "./audit-rows.js";
|
|
12
12
|
export { reviewAuditRowKeys } from "./audit-rows.js";
|
|
13
13
|
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
|
|
14
14
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
15
|
-
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
15
|
+
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
16
16
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
17
17
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
18
18
|
export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
@@ -27,11 +27,15 @@ export function buildReviewDecision(state) {
|
|
|
27
27
|
throw new Error("Could not confirm requires a non-empty reason.");
|
|
28
28
|
}
|
|
29
29
|
const candidate = candidateForDecision(state.item, state.decision);
|
|
30
|
+
// A decision that selects no candidate (a value-neutral decision on a
|
|
31
|
+
// conflict) records no candidate id and no candidate's projection hints.
|
|
32
|
+
const selectsNone = decisionSelectsNoCandidate(state.item, state.decision);
|
|
30
33
|
const projection = {
|
|
31
|
-
...candidate.projection,
|
|
32
|
-
reviewOutcomeId: candidate.projection?.reviewOutcomeId
|
|
34
|
+
...(selectsNone ? state.item.spec.projection : candidate.projection),
|
|
35
|
+
reviewOutcomeId: (selectsNone ? state.item.spec.projection : candidate.projection)?.reviewOutcomeId
|
|
33
36
|
?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
|
|
34
37
|
};
|
|
38
|
+
const candidateProjection = selectsNone ? undefined : candidate.projection;
|
|
35
39
|
const authorizing = buildDecisionCardAuthorizing(state);
|
|
36
40
|
const reviewDecision = {
|
|
37
41
|
apiVersion: reviewResourceApiVersion,
|
|
@@ -43,7 +47,7 @@ export function buildReviewDecision(state) {
|
|
|
43
47
|
},
|
|
44
48
|
spec: {
|
|
45
49
|
reviewItemName: state.item.metadata.name,
|
|
46
|
-
candidateId: candidate.id,
|
|
50
|
+
...(selectsNone ? {} : { candidateId: candidate.id }),
|
|
47
51
|
status: definition.status,
|
|
48
52
|
...(state.decision === "could-not-confirm"
|
|
49
53
|
? {
|
|
@@ -64,7 +68,7 @@ export function buildReviewDecision(state) {
|
|
|
64
68
|
: {}),
|
|
65
69
|
},
|
|
66
70
|
status: {
|
|
67
|
-
...(
|
|
71
|
+
...(candidateProjection?.claimId ? { appliedToClaimIds: [candidateProjection.claimId] } : {}),
|
|
68
72
|
},
|
|
69
73
|
};
|
|
70
74
|
assertReviewResolutionConsistency(`ReviewDecision ${reviewDecision.metadata.name}`, {
|
|
@@ -125,8 +129,24 @@ function buildDecisionCardAuthorizing(state) {
|
|
|
125
129
|
* followed by the selected decision label so the block is self-contained.
|
|
126
130
|
*/
|
|
127
131
|
function decisionCardRenderedPrompt(state, targetLabel) {
|
|
132
|
+
// The card shows proposals excluded at import; the recorded prompt must say
|
|
133
|
+
// the reviewer was told about them.
|
|
134
|
+
const excluded = excludedProposalsSentence(buildReviewItemPresentation(state.item).excludedProposals);
|
|
135
|
+
const base = decisionCardBasePrompt(state, targetLabel);
|
|
136
|
+
return excluded ? `${base} ${excluded}` : base;
|
|
137
|
+
}
|
|
138
|
+
function decisionCardBasePrompt(state, targetLabel) {
|
|
128
139
|
const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
|
|
129
|
-
const
|
|
140
|
+
const proposedCandidates = state.item.spec.candidates.filter((c) => c.role === "proposed");
|
|
141
|
+
// A conflict card lists every proposed value and offers reject-all or could
|
|
142
|
+
// not confirm; the prompt states the same, naming no single value.
|
|
143
|
+
if (proposedCandidates.length > 1) {
|
|
144
|
+
const decisionLabel = state.decision === "reject-proposed"
|
|
145
|
+
? "Reject all values"
|
|
146
|
+
: state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
147
|
+
return `For ${targetLabel}, ${proposedCandidates.length} different values were proposed: ${proposedCandidates.map((c) => formatValue(c.value)).join(", ")}. Selected decision: ${decisionLabel}.`;
|
|
148
|
+
}
|
|
149
|
+
const proposedCandidate = proposedCandidates[0];
|
|
130
150
|
const currentValue = formatValue(currentCandidate?.value ?? "");
|
|
131
151
|
const proposedValue = formatValue(proposedCandidate?.value ?? "");
|
|
132
152
|
const decisionLabel = state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
@@ -206,7 +226,9 @@ function duplicateTargetIssues(items) {
|
|
|
206
226
|
return issues;
|
|
207
227
|
}
|
|
208
228
|
function mapReviewApplyResultToActions(input) {
|
|
209
|
-
|
|
229
|
+
// Could-not-confirm applies nothing, and neither does a decision that selects
|
|
230
|
+
// no candidate (rejecting every value of a conflict): there is no value to apply.
|
|
231
|
+
if (input.result.decision === "could-not-confirm" || input.result.selectedCandidateId === undefined) {
|
|
210
232
|
return [];
|
|
211
233
|
}
|
|
212
234
|
const context = buildReviewApplyActionContext(input.result, input.itemByName, input.issues);
|
|
@@ -256,6 +278,8 @@ function buildReviewApplyActionContext(result, itemByName, issues) {
|
|
|
256
278
|
};
|
|
257
279
|
}
|
|
258
280
|
function matchingSelectedCandidate(item, result) {
|
|
281
|
+
if (result.selectedCandidateId === undefined)
|
|
282
|
+
return undefined;
|
|
259
283
|
const candidate = findSoleCandidateById(item, result.selectedCandidateId);
|
|
260
284
|
return candidate
|
|
261
285
|
&& candidate.role === result.selectedCandidateRole
|
|
@@ -280,6 +304,16 @@ export function buildReviewWorkbenchResultsFromSession(session) {
|
|
|
280
304
|
if (!reviewDecision) {
|
|
281
305
|
return [];
|
|
282
306
|
}
|
|
307
|
+
if (decisionSelectsNoCandidate(item, decision)) {
|
|
308
|
+
return [{
|
|
309
|
+
reviewItemName: item.metadata.name,
|
|
310
|
+
decision,
|
|
311
|
+
unselectedCandidates: [...item.spec.candidates],
|
|
312
|
+
reviewDecision,
|
|
313
|
+
status: reviewDecision.spec.status,
|
|
314
|
+
rationale: reviewDecision.spec.rationale,
|
|
315
|
+
}];
|
|
316
|
+
}
|
|
283
317
|
const selectedCandidate = candidateForDecision(item, decision);
|
|
284
318
|
const editedValue = decision === "accept-proposed"
|
|
285
319
|
? session.editedValuesByItemName?.[item.metadata.name]
|
|
@@ -491,13 +525,13 @@ function fieldCardState(item, decision) {
|
|
|
491
525
|
* {@link keepActionDecision}). Saying "Kept — flagged wrong" there would claim a
|
|
492
526
|
* distinction the record does not hold, so the chip states only what happened.
|
|
493
527
|
*/
|
|
494
|
-
function chipLabel(state, hasCurrentValue) {
|
|
528
|
+
function chipLabel(state, hasCurrentValue, conflictingValues) {
|
|
495
529
|
switch (state) {
|
|
496
530
|
case "accepted": return "Accepted";
|
|
497
|
-
case "kept": return hasCurrentValue ? "Kept current" : "Left unset";
|
|
498
|
-
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : "Left unset";
|
|
531
|
+
case "kept": return hasCurrentValue ? "Kept current" : conflictingValues ? "All values rejected" : "Left unset";
|
|
532
|
+
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : conflictingValues ? "All values rejected" : "Left unset";
|
|
499
533
|
case "could-not-confirm": return "Could not confirm";
|
|
500
|
-
default: return "Needs review";
|
|
534
|
+
default: return conflictingValues ? `Conflict: ${conflictingValues} values` : "Needs review";
|
|
501
535
|
}
|
|
502
536
|
}
|
|
503
537
|
const DEFAULT_REVIEW_QUEUE_PAGE_SIZE = 50;
|
|
@@ -618,11 +652,16 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
618
652
|
const state = fieldCardState(item, decision);
|
|
619
653
|
const decided = decision !== undefined;
|
|
620
654
|
const current = item.spec.candidates.find((candidate) => candidate.role === "current");
|
|
621
|
-
const
|
|
655
|
+
const proposedCandidates = item.spec.candidates.filter((candidate) => candidate.role === "proposed");
|
|
656
|
+
// Several proposed values for one claim are a conflict. A control names a
|
|
657
|
+
// role, not a value, so none may make one of them the trusted value; the
|
|
658
|
+
// reviewer can still reject them all or end the round as could-not-confirm.
|
|
659
|
+
const conflict = proposedCandidates.length > 1;
|
|
660
|
+
const proposed = proposedCandidates.length === 1 ? proposedCandidates[0] : undefined;
|
|
622
661
|
const presentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
623
662
|
const hasCurrentValue = current !== undefined && !isEmptyValue(current.value);
|
|
624
663
|
const kind = hasCurrentValue ? "Update" : "New";
|
|
625
|
-
const keepLabel = hasCurrentValue ? "Keep current" : "Leave unset";
|
|
664
|
+
const keepLabel = hasCurrentValue ? "Keep current" : conflict ? "Reject all values" : "Leave unset";
|
|
626
665
|
// Only render controls whose decision this item can actually record. Every
|
|
627
666
|
// decision resolves to a candidate role, so a control routed at a role the
|
|
628
667
|
// item does not carry cannot emit anything — it used to throw mid-click and
|
|
@@ -653,12 +692,15 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
653
692
|
<div class="frow1">
|
|
654
693
|
<span class="fname">${escapeHtml(presentation.targetLabel)}</span>
|
|
655
694
|
<span class="fkind">${kind}</span>
|
|
656
|
-
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
695
|
+
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state, hasCurrentValue, conflict ? proposedCandidates.length : undefined)}</span>
|
|
657
696
|
</div>
|
|
658
697
|
${proposed
|
|
659
698
|
? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
|
|
660
|
-
:
|
|
699
|
+
: proposedCandidates.length > 1
|
|
700
|
+
? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel)
|
|
701
|
+
: "<p class=\"field-value\">No proposed value is available for this field.</p>"}
|
|
661
702
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
703
|
+
${renderExtractionImportNotes(presentation)}
|
|
662
704
|
<div class="decide">
|
|
663
705
|
${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
|
|
664
706
|
${proposed ? `<button class="btn use" type="button" data-testid="use-proposed" data-item-name="${escapeHtml(item.metadata.name)}">Use proposed</button>
|
|
@@ -667,6 +709,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
667
709
|
Suggestion was wrong
|
|
668
710
|
</label>
|
|
669
711
|
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
712
|
+
${conflict ? `<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
670
713
|
</div>
|
|
671
714
|
<!--
|
|
672
715
|
A control's precondition message belongs where the control is. The
|
|
@@ -677,7 +720,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
677
720
|
-->
|
|
678
721
|
<span class="derr" data-testid="decision-error" role="alert" hidden></span>
|
|
679
722
|
<div class="decided">
|
|
680
|
-
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
723
|
+
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue, conflict ? proposedCandidates.length : undefined)}</span>
|
|
681
724
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
682
725
|
</div>
|
|
683
726
|
${renderAuditDetails(item, current, proposed, session, presentationAdapter)}
|
|
@@ -685,6 +728,24 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
685
728
|
</section>
|
|
686
729
|
`;
|
|
687
730
|
}
|
|
731
|
+
/**
|
|
732
|
+
* The card body for an item whose candidate set holds several proposed values.
|
|
733
|
+
* Lists every value with its excerpt; the workbench records a decision against
|
|
734
|
+
* a role, so it cannot pick one of them, and says what the reviewer can do.
|
|
735
|
+
*/
|
|
736
|
+
function renderConflictingProposals(item, candidates, presentationAdapter, targetLabel) {
|
|
737
|
+
const values = candidates.map((candidate) => {
|
|
738
|
+
const text = buildReviewCandidatePresentation(item, candidate, presentationAdapter, targetLabel).valueText;
|
|
739
|
+
const excerpt = candidate.locator?.excerpt;
|
|
740
|
+
return `<li data-testid="conflicting-value"><span class="vtext">${escapeHtml(text)}</span>${excerpt ? ` <q>${escapeHtml(excerpt)}</q>` : ""}</li>`;
|
|
741
|
+
}).join("");
|
|
742
|
+
return `
|
|
743
|
+
<div class="field-value" data-testid="conflicting-proposals">
|
|
744
|
+
<p>${candidates.length} different values were proposed for this field. This queue cannot choose one of them yet: reject them all, or mark the field Could not confirm with a reason.</p>
|
|
745
|
+
<ul>${values}</ul>
|
|
746
|
+
</div>
|
|
747
|
+
`;
|
|
748
|
+
}
|
|
688
749
|
/**
|
|
689
750
|
* Renders the Current → Proposed diff row. The proposed side is editable
|
|
690
751
|
* (via {@link renderProposedValueEditor}) until the field has a decision.
|
|
@@ -871,6 +932,35 @@ function renderProvenanceRow(item, proposed, presentationAdapter) {
|
|
|
871
932
|
</div>
|
|
872
933
|
`;
|
|
873
934
|
}
|
|
935
|
+
/**
|
|
936
|
+
* What an envelope import says about this field's evidence: whether its
|
|
937
|
+
* excerpts were checked against the prepared source text, and any rival
|
|
938
|
+
* proposals the import left out because their excerpt was not at their span.
|
|
939
|
+
* Such a proposal is unverifiable, not disproven, so the reviewer sees it
|
|
940
|
+
* before accepting the value that remained. Empty for items from other
|
|
941
|
+
* producers.
|
|
942
|
+
*/
|
|
943
|
+
function renderExtractionImportNotes(presentation) {
|
|
944
|
+
const { excerptVerification: verification, excludedProposals: excluded } = presentation;
|
|
945
|
+
if (verification === undefined && excluded.length === 0)
|
|
946
|
+
return "";
|
|
947
|
+
const excludedHtml = excluded.length === 0 ? "" : `
|
|
948
|
+
<div class="noprov" data-testid="excluded-proposals" role="note">
|
|
949
|
+
${WARNING_SVG}
|
|
950
|
+
<span class="tag">${excluded.length} excluded</span>
|
|
951
|
+
<span>${excluded.length === 1 ? "Another proposal for this field was" : `${excluded.length} other proposals for this field were`} left out because the source text at the cited span is not the excerpt:
|
|
952
|
+
${excluded.map((entry) => `<span data-testid="excluded-proposal" data-proposal-index="${escapeHtml(String(entry.proposalIndex))}"><q>${escapeHtml(entry.valueText)}</q> (proposal ${escapeHtml(String(entry.proposalIndex))}, ${escapeHtml(entry.locator)})</span>`).join(", ")}.
|
|
953
|
+
Unverifiable is not disproven: check the source before deciding.</span>
|
|
954
|
+
</div>`;
|
|
955
|
+
const verificationHtml = verification === undefined ? "" : `
|
|
956
|
+
<p class="excerpt-verification" data-testid="excerpt-verification" data-verified="${verification === "verified" ? "true" : "false"}">${verification === "verified"
|
|
957
|
+
? "Excerpts checked against the prepared source text at import."
|
|
958
|
+
: "Excerpts not checked against the prepared source text at import."}</p>`;
|
|
959
|
+
return `
|
|
960
|
+
<div class="prov import-notes">${excludedHtml}${verificationHtml}
|
|
961
|
+
</div>
|
|
962
|
+
`;
|
|
963
|
+
}
|
|
874
964
|
function renderConfidenceMeter(confidence) {
|
|
875
965
|
const clamped = Math.max(0, Math.min(1, confidence));
|
|
876
966
|
const pct = Math.round(clamped * 100);
|
|
@@ -927,6 +1017,7 @@ function renderAuditDetails(item, current, proposed, session, presentationAdapte
|
|
|
927
1017
|
${proposed?.extraction.model ? fieldItem("model", "Model", proposed.extraction.model) : ""}
|
|
928
1018
|
${proposed ? placementItem(trace, { of: proposed.id, property: "extraction.extractor" }, "extractor", "Extractor", proposed.extraction.extractor ?? "unknown") : ""}
|
|
929
1019
|
${proposed ? fieldItem("extracted-at", "Extracted at", proposed.extraction.extractedAt ?? "unknown") : ""}
|
|
1020
|
+
${buildReviewItemPresentation(item, presentationAdapter).excludedProposals.map((entry) => fieldItem("excluded-proposal", `Excluded proposal ${entry.proposalIndex}`, `${entry.valueText} at ${entry.locator}: excerpt not at the cited span`)).join("")}
|
|
930
1021
|
</dl>
|
|
931
1022
|
${preview
|
|
932
1023
|
? `<div class="preview-section-grid">${renderSurfacePreviewSections(preview, trace)}</div><p class="preview-disclaimer preview-disclaimer-footer">${escapeHtml(preview.postureDisclaimer)}</p>`
|
|
@@ -1165,7 +1256,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1165
1256
|
const session = sessionForEvent;
|
|
1166
1257
|
const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
|
|
1167
1258
|
const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
|
|
1168
|
-
const
|
|
1259
|
+
const candidateId = item && decision ? decisionCandidateId(item, decision) : undefined;
|
|
1169
1260
|
const definition = decision ? workbenchDecisionDefinitions[decision] : undefined;
|
|
1170
1261
|
const note = itemName ? session.notesByItemName[itemName] : undefined;
|
|
1171
1262
|
// Carry the reviewer's inline edit in the event (accept-proposed only), so
|
|
@@ -1188,7 +1279,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1188
1279
|
occurredAt: session.reviewedAt,
|
|
1189
1280
|
reviewItemName: itemName,
|
|
1190
1281
|
reviewDecisionName: item && decision ? `${item.metadata.name}-${decision}` : undefined,
|
|
1191
|
-
candidateId
|
|
1282
|
+
candidateId,
|
|
1192
1283
|
status: definition?.status,
|
|
1193
1284
|
...(decision === "could-not-confirm"
|
|
1194
1285
|
? {
|
|
@@ -38,7 +38,10 @@
|
|
|
38
38
|
.inspector-heading, .inspector-layout { display: grid; grid-template-columns: minmax(15rem, .7fr) minmax(0, 1.3fr); gap: 1rem; }
|
|
39
39
|
.inspector-heading h2 { margin: 0; }
|
|
40
40
|
.inspector-posture { display: flex; flex-direction: column; padding: .75rem; border-radius: var(--k-radius-sm); background: var(--k-positive-wash); }
|
|
41
|
-
|
|
41
|
+
/* Caution first: an artifact failure on the same source must win the tie and stay negative. */
|
|
42
|
+
.inspector-posture.extraction-incomplete { background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
43
|
+
.inspector-posture.digest-mismatch, .inspector-posture.artifact-unavailable, .inspector-posture.excerpt-mismatch, .inspector-posture.proposals-excluded { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
44
|
+
.inspector-posture.extraction-failed { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
42
45
|
.inspector-filters { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
43
46
|
.inspector-filters label { display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
44
47
|
.inspector-filters select, .inspector-filters input { color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -624,6 +627,12 @@ h1, h2, h3, p {
|
|
|
624
627
|
font-size: 12px;
|
|
625
628
|
}
|
|
626
629
|
|
|
630
|
+
.excerpt-verification {
|
|
631
|
+
margin: 0;
|
|
632
|
+
font-size: 12px;
|
|
633
|
+
color: var(--k-muted);
|
|
634
|
+
}
|
|
635
|
+
|
|
627
636
|
/* decisions */
|
|
628
637
|
|
|
629
638
|
/* The decision row is the point of the card, so it sits on its own line,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export function toSurfaceReviewedExtractionImport(record) {
|
|
2
|
+
const unreported = record.spec.envelope.result.proposals.findIndex((proposal) => proposal.confidence === undefined);
|
|
3
|
+
if (unreported !== -1) {
|
|
4
|
+
throw new Error(`Surface's reviewed-extraction profile requires a proposer confidence on every proposal; proposal ${unreported} of ${record.metadata.name} reports none.`);
|
|
5
|
+
}
|
|
2
6
|
return record;
|
|
3
7
|
}
|
|
4
8
|
export function toSurfaceReviewedExtractionItem(item) {
|