@kontourai/survey 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/example-data/public-directory-review-resource.d.ts +3 -3
- package/dist/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 +59 -3
- package/dist/src/extraction-envelope.js +159 -21
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +1 -1
- package/dist/src/mcp/review-mcp.js +42 -25
- package/dist/src/review-workbench/extraction-inspector.d.ts +15 -1
- package/dist/src/review-workbench/extraction-inspector.js +55 -19
- package/dist/src/review-workbench/queue-binding.js +1 -1
- package/dist/src/review-workbench/review-presentation.d.ts +2 -1
- package/dist/src/review-workbench/review-presentation.js +23 -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 +2 -0
- package/dist/src/review-workbench/review-workbench.css +2 -0
- package/dist/src/review-workbench/review-workbench.d.ts +14 -7
- package/dist/src/review-workbench/review-workbench.js +72 -19
- package/dist/src/review-workbench/review-workbench.standalone.css +2 -0
- 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
|
@@ -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,6 +1,6 @@
|
|
|
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";
|
|
@@ -12,7 +12,7 @@ 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}`, {
|
|
@@ -126,7 +130,16 @@ function buildDecisionCardAuthorizing(state) {
|
|
|
126
130
|
*/
|
|
127
131
|
function decisionCardRenderedPrompt(state, targetLabel) {
|
|
128
132
|
const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
|
|
129
|
-
const
|
|
133
|
+
const proposedCandidates = state.item.spec.candidates.filter((c) => c.role === "proposed");
|
|
134
|
+
// A conflict card lists every proposed value and offers reject-all or could
|
|
135
|
+
// not confirm; the prompt states the same, naming no single value.
|
|
136
|
+
if (proposedCandidates.length > 1) {
|
|
137
|
+
const decisionLabel = state.decision === "reject-proposed"
|
|
138
|
+
? "Reject all values"
|
|
139
|
+
: state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
140
|
+
return `For ${targetLabel}, ${proposedCandidates.length} different values were proposed: ${proposedCandidates.map((c) => formatValue(c.value)).join(", ")}. Selected decision: ${decisionLabel}.`;
|
|
141
|
+
}
|
|
142
|
+
const proposedCandidate = proposedCandidates[0];
|
|
130
143
|
const currentValue = formatValue(currentCandidate?.value ?? "");
|
|
131
144
|
const proposedValue = formatValue(proposedCandidate?.value ?? "");
|
|
132
145
|
const decisionLabel = state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
@@ -206,7 +219,9 @@ function duplicateTargetIssues(items) {
|
|
|
206
219
|
return issues;
|
|
207
220
|
}
|
|
208
221
|
function mapReviewApplyResultToActions(input) {
|
|
209
|
-
|
|
222
|
+
// Could-not-confirm applies nothing, and neither does a decision that selects
|
|
223
|
+
// no candidate (rejecting every value of a conflict): there is no value to apply.
|
|
224
|
+
if (input.result.decision === "could-not-confirm" || input.result.selectedCandidateId === undefined) {
|
|
210
225
|
return [];
|
|
211
226
|
}
|
|
212
227
|
const context = buildReviewApplyActionContext(input.result, input.itemByName, input.issues);
|
|
@@ -256,6 +271,8 @@ function buildReviewApplyActionContext(result, itemByName, issues) {
|
|
|
256
271
|
};
|
|
257
272
|
}
|
|
258
273
|
function matchingSelectedCandidate(item, result) {
|
|
274
|
+
if (result.selectedCandidateId === undefined)
|
|
275
|
+
return undefined;
|
|
259
276
|
const candidate = findSoleCandidateById(item, result.selectedCandidateId);
|
|
260
277
|
return candidate
|
|
261
278
|
&& candidate.role === result.selectedCandidateRole
|
|
@@ -280,6 +297,16 @@ export function buildReviewWorkbenchResultsFromSession(session) {
|
|
|
280
297
|
if (!reviewDecision) {
|
|
281
298
|
return [];
|
|
282
299
|
}
|
|
300
|
+
if (decisionSelectsNoCandidate(item, decision)) {
|
|
301
|
+
return [{
|
|
302
|
+
reviewItemName: item.metadata.name,
|
|
303
|
+
decision,
|
|
304
|
+
unselectedCandidates: [...item.spec.candidates],
|
|
305
|
+
reviewDecision,
|
|
306
|
+
status: reviewDecision.spec.status,
|
|
307
|
+
rationale: reviewDecision.spec.rationale,
|
|
308
|
+
}];
|
|
309
|
+
}
|
|
283
310
|
const selectedCandidate = candidateForDecision(item, decision);
|
|
284
311
|
const editedValue = decision === "accept-proposed"
|
|
285
312
|
? session.editedValuesByItemName?.[item.metadata.name]
|
|
@@ -491,13 +518,13 @@ function fieldCardState(item, decision) {
|
|
|
491
518
|
* {@link keepActionDecision}). Saying "Kept — flagged wrong" there would claim a
|
|
492
519
|
* distinction the record does not hold, so the chip states only what happened.
|
|
493
520
|
*/
|
|
494
|
-
function chipLabel(state, hasCurrentValue) {
|
|
521
|
+
function chipLabel(state, hasCurrentValue, conflictingValues) {
|
|
495
522
|
switch (state) {
|
|
496
523
|
case "accepted": return "Accepted";
|
|
497
|
-
case "kept": return hasCurrentValue ? "Kept current" : "Left unset";
|
|
498
|
-
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : "Left unset";
|
|
524
|
+
case "kept": return hasCurrentValue ? "Kept current" : conflictingValues ? "All values rejected" : "Left unset";
|
|
525
|
+
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : conflictingValues ? "All values rejected" : "Left unset";
|
|
499
526
|
case "could-not-confirm": return "Could not confirm";
|
|
500
|
-
default: return "Needs review";
|
|
527
|
+
default: return conflictingValues ? `Conflict: ${conflictingValues} values` : "Needs review";
|
|
501
528
|
}
|
|
502
529
|
}
|
|
503
530
|
const DEFAULT_REVIEW_QUEUE_PAGE_SIZE = 50;
|
|
@@ -618,11 +645,16 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
618
645
|
const state = fieldCardState(item, decision);
|
|
619
646
|
const decided = decision !== undefined;
|
|
620
647
|
const current = item.spec.candidates.find((candidate) => candidate.role === "current");
|
|
621
|
-
const
|
|
648
|
+
const proposedCandidates = item.spec.candidates.filter((candidate) => candidate.role === "proposed");
|
|
649
|
+
// Several proposed values for one claim are a conflict. A control names a
|
|
650
|
+
// role, not a value, so none may make one of them the trusted value; the
|
|
651
|
+
// reviewer can still reject them all or end the round as could-not-confirm.
|
|
652
|
+
const conflict = proposedCandidates.length > 1;
|
|
653
|
+
const proposed = proposedCandidates.length === 1 ? proposedCandidates[0] : undefined;
|
|
622
654
|
const presentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
623
655
|
const hasCurrentValue = current !== undefined && !isEmptyValue(current.value);
|
|
624
656
|
const kind = hasCurrentValue ? "Update" : "New";
|
|
625
|
-
const keepLabel = hasCurrentValue ? "Keep current" : "Leave unset";
|
|
657
|
+
const keepLabel = hasCurrentValue ? "Keep current" : conflict ? "Reject all values" : "Leave unset";
|
|
626
658
|
// Only render controls whose decision this item can actually record. Every
|
|
627
659
|
// decision resolves to a candidate role, so a control routed at a role the
|
|
628
660
|
// item does not carry cannot emit anything — it used to throw mid-click and
|
|
@@ -653,11 +685,13 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
653
685
|
<div class="frow1">
|
|
654
686
|
<span class="fname">${escapeHtml(presentation.targetLabel)}</span>
|
|
655
687
|
<span class="fkind">${kind}</span>
|
|
656
|
-
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
688
|
+
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state, hasCurrentValue, conflict ? proposedCandidates.length : undefined)}</span>
|
|
657
689
|
</div>
|
|
658
690
|
${proposed
|
|
659
691
|
? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
|
|
660
|
-
:
|
|
692
|
+
: proposedCandidates.length > 1
|
|
693
|
+
? renderConflictingProposals(item, proposedCandidates, presentationAdapter, presentation.targetLabel)
|
|
694
|
+
: "<p class=\"field-value\">No proposed value is available for this field.</p>"}
|
|
661
695
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
662
696
|
<div class="decide">
|
|
663
697
|
${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
|
|
@@ -667,6 +701,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
667
701
|
Suggestion was wrong
|
|
668
702
|
</label>
|
|
669
703
|
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
704
|
+
${conflict ? `<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
670
705
|
</div>
|
|
671
706
|
<!--
|
|
672
707
|
A control's precondition message belongs where the control is. The
|
|
@@ -677,7 +712,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
677
712
|
-->
|
|
678
713
|
<span class="derr" data-testid="decision-error" role="alert" hidden></span>
|
|
679
714
|
<div class="decided">
|
|
680
|
-
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
715
|
+
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue, conflict ? proposedCandidates.length : undefined)}</span>
|
|
681
716
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
682
717
|
</div>
|
|
683
718
|
${renderAuditDetails(item, current, proposed, session, presentationAdapter)}
|
|
@@ -685,6 +720,24 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
685
720
|
</section>
|
|
686
721
|
`;
|
|
687
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* The card body for an item whose candidate set holds several proposed values.
|
|
725
|
+
* Lists every value with its excerpt; the workbench records a decision against
|
|
726
|
+
* a role, so it cannot pick one of them, and says what the reviewer can do.
|
|
727
|
+
*/
|
|
728
|
+
function renderConflictingProposals(item, candidates, presentationAdapter, targetLabel) {
|
|
729
|
+
const values = candidates.map((candidate) => {
|
|
730
|
+
const text = buildReviewCandidatePresentation(item, candidate, presentationAdapter, targetLabel).valueText;
|
|
731
|
+
const excerpt = candidate.locator?.excerpt;
|
|
732
|
+
return `<li data-testid="conflicting-value"><span class="vtext">${escapeHtml(text)}</span>${excerpt ? ` <q>${escapeHtml(excerpt)}</q>` : ""}</li>`;
|
|
733
|
+
}).join("");
|
|
734
|
+
return `
|
|
735
|
+
<div class="field-value" data-testid="conflicting-proposals">
|
|
736
|
+
<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>
|
|
737
|
+
<ul>${values}</ul>
|
|
738
|
+
</div>
|
|
739
|
+
`;
|
|
740
|
+
}
|
|
688
741
|
/**
|
|
689
742
|
* Renders the Current → Proposed diff row. The proposed side is editable
|
|
690
743
|
* (via {@link renderProposedValueEditor}) until the field has a decision.
|
|
@@ -1165,7 +1218,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1165
1218
|
const session = sessionForEvent;
|
|
1166
1219
|
const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
|
|
1167
1220
|
const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
|
|
1168
|
-
const
|
|
1221
|
+
const candidateId = item && decision ? decisionCandidateId(item, decision) : undefined;
|
|
1169
1222
|
const definition = decision ? workbenchDecisionDefinitions[decision] : undefined;
|
|
1170
1223
|
const note = itemName ? session.notesByItemName[itemName] : undefined;
|
|
1171
1224
|
// Carry the reviewer's inline edit in the event (accept-proposed only), so
|
|
@@ -1188,7 +1241,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1188
1241
|
occurredAt: session.reviewedAt,
|
|
1189
1242
|
reviewItemName: itemName,
|
|
1190
1243
|
reviewDecisionName: item && decision ? `${item.metadata.name}-${decision}` : undefined,
|
|
1191
|
-
candidateId
|
|
1244
|
+
candidateId,
|
|
1192
1245
|
status: definition?.status,
|
|
1193
1246
|
...(decision === "could-not-confirm"
|
|
1194
1247
|
? {
|
|
@@ -39,6 +39,8 @@
|
|
|
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
|
.inspector-posture.digest-mismatch, .inspector-posture.artifact-unavailable, .inspector-posture.excerpt-mismatch { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
42
|
+
.inspector-posture.extraction-failed { background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
43
|
+
.inspector-posture.extraction-incomplete { background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
42
44
|
.inspector-filters { display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
43
45
|
.inspector-filters label { display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
44
46
|
.inspector-filters select, .inspector-filters input { color: var(--k-text); background: var(--k-sunken); border: 1px solid var(--k-line); padding: .4rem; }
|
|
@@ -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) {
|
package/dist/src/to-surface.js
CHANGED
|
@@ -44,6 +44,10 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
44
44
|
const events = [];
|
|
45
45
|
for (const projection of input.claims) {
|
|
46
46
|
const candidateSet = requireMapValue(candidateSets, projection.candidateSetId, "candidate set");
|
|
47
|
+
if (projection.candidateId === undefined && candidateSet.selectedCandidateId === undefined && candidateSet.candidates.length > 1) {
|
|
48
|
+
projectUnselectedSetClaim({ input, projection, candidateSet, rawSources, extractions, reviewsByCandidateSet, projectionContextId, claims, evidence, events });
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
47
51
|
const candidate = selectCandidate(candidateSet, projection.candidateId);
|
|
48
52
|
const extraction = requireMapValue(extractions, candidate.extractionId, "extraction");
|
|
49
53
|
const rawSource = requireMapValue(rawSources, extraction.sourceId, "raw source");
|
|
@@ -220,6 +224,116 @@ export function buildSurveyTrustBundle(input, options = {}) {
|
|
|
220
224
|
events,
|
|
221
225
|
};
|
|
222
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* A claim over a candidate set that selects none of its candidates (a review
|
|
229
|
+
* rejected every conflicting value, or could not confirm any). No single value
|
|
230
|
+
* is presented: the claim value is the projection's own (null from the
|
|
231
|
+
* canonical path), every candidate is listed in `metadata.survey.candidates`
|
|
232
|
+
* and backed by its own evidence record, and the status can never be trusted.
|
|
233
|
+
* A review that names one candidate cannot apply to such a claim and is
|
|
234
|
+
* refused rather than dropped. With `reviewProofs`, no integrity anchor is
|
|
235
|
+
* attached: the anchor commits one reviewed candidate, and this claim has none
|
|
236
|
+
* (it is never `verified` or `assumed`).
|
|
237
|
+
*/
|
|
238
|
+
function projectUnselectedSetClaim(context) {
|
|
239
|
+
const { input, projection, candidateSet, projectionContextId } = context;
|
|
240
|
+
const setReviews = context.reviewsByCandidateSet.get(candidateSet.id) ?? [];
|
|
241
|
+
const candidateReviews = setReviews.filter((review) => review.candidateId);
|
|
242
|
+
if (candidateReviews.length) {
|
|
243
|
+
throw new Error(`Claim ${projection.id} names no candidate of set ${candidateSet.id}, but review ${candidateReviews.map((review) => review.id).join(", ")} is about candidate ${candidateReviews.map((review) => review.candidateId).join(", ")}: a candidate-level review needs a selectedCandidateId on the set or a candidateId on the claim.`);
|
|
244
|
+
}
|
|
245
|
+
const reviews = setReviews;
|
|
246
|
+
const review = latestReview(reviews, projection.id);
|
|
247
|
+
const setStatus = statusFor({ candidateSet, candidate: { id: "", extractionId: "", value: null } });
|
|
248
|
+
const status = projection.status
|
|
249
|
+
?? (review?.resolution === "could_not_confirm"
|
|
250
|
+
? (setStatus === "disputed" ? setStatus : review.status)
|
|
251
|
+
: review?.status ?? setStatus);
|
|
252
|
+
if (status === "verified" || status === "assumed") {
|
|
253
|
+
throw new ReviewAgreementError("status-mismatch", `Claim ${projection.id} selects no candidate of set ${candidateSet.id}, so it cannot be ${status}`);
|
|
254
|
+
}
|
|
255
|
+
assertReviewOutcomeDiscipline({ subject: `Claim ${projection.id}`, status, review, candidateSetStatus: candidateSet.status });
|
|
256
|
+
const sources = candidateSet.candidates.map((candidate) => {
|
|
257
|
+
const extraction = requireMapValue(context.extractions, candidate.extractionId, "extraction");
|
|
258
|
+
const rawSource = requireMapValue(context.rawSources, extraction.sourceId, "raw source");
|
|
259
|
+
return { candidate, extraction, rawSource };
|
|
260
|
+
});
|
|
261
|
+
const evidenceIds = sources.map(({ candidate, extraction, rawSource }) => {
|
|
262
|
+
const id = projectionRecordId(projection.id, projectionContextId, "claim-evidence", `evidence.source.${candidate.id}`);
|
|
263
|
+
const policyStandard = policyStandardFields(rawSource);
|
|
264
|
+
context.evidence.push({
|
|
265
|
+
id,
|
|
266
|
+
claimId: projection.id,
|
|
267
|
+
evidenceType: projection.evidenceType ?? (rawSource.resolution ? evidenceTypeForResolution(rawSource, rawSource.resolution) : evidenceTypeFor(rawSource)),
|
|
268
|
+
method: projection.evidenceMethod ?? "extraction",
|
|
269
|
+
sourceRef: rawSource.sourceRef,
|
|
270
|
+
sourceLocator: extraction.locator,
|
|
271
|
+
excerptOrSummary: evidenceExcerptOrSummary({ rawSource, extraction, projection, policyStandard }),
|
|
272
|
+
observedAt: rawSource.observedAt,
|
|
273
|
+
collectedBy: projection.collectedBy,
|
|
274
|
+
integrityRef: rawSource.checksum,
|
|
275
|
+
metadata: {
|
|
276
|
+
...rawSource.metadata,
|
|
277
|
+
...extraction.metadata,
|
|
278
|
+
...candidate.metadata,
|
|
279
|
+
...(policyStandard ? { policyStandard } : {}),
|
|
280
|
+
rawSourceKind: rawSource.kind,
|
|
281
|
+
locatorScheme: rawSource.locatorScheme,
|
|
282
|
+
candidateId: candidate.id,
|
|
283
|
+
...(candidate.confidence ?? extraction.confidence) !== undefined ? { confidence: candidate.confidence ?? extraction.confidence } : {},
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
return id;
|
|
287
|
+
});
|
|
288
|
+
const producerSurvey = isRecord(projection.metadata?.survey) ? projection.metadata.survey : {};
|
|
289
|
+
context.claims.push({
|
|
290
|
+
id: projection.id,
|
|
291
|
+
subjectType: projection.subjectType,
|
|
292
|
+
subjectId: projection.subjectId,
|
|
293
|
+
facet: projection.facet,
|
|
294
|
+
claimType: projection.claimType,
|
|
295
|
+
fieldOrBehavior: projection.fieldOrBehavior,
|
|
296
|
+
value: "value" in projection ? projection.value : null,
|
|
297
|
+
status,
|
|
298
|
+
createdAt: projection.createdAt ?? sources.map(({ extraction }) => extraction.extractedAt).sort()[0],
|
|
299
|
+
updatedAt: projection.updatedAt ?? review?.reviewedAt ?? input.generatedAt,
|
|
300
|
+
impactLevel: projection.impactLevel,
|
|
301
|
+
derivedFrom: projection.derivedFrom,
|
|
302
|
+
derivationEdges: projection.derivationEdges,
|
|
303
|
+
confidenceBasis: {
|
|
304
|
+
sourceQuality: "moderate",
|
|
305
|
+
reviewerAuthority: "none",
|
|
306
|
+
evidenceStrength: "weak",
|
|
307
|
+
impactLevel: projection.impactLevel,
|
|
308
|
+
...projection.confidenceBasis,
|
|
309
|
+
},
|
|
310
|
+
metadata: {
|
|
311
|
+
...projection.metadata,
|
|
312
|
+
survey: {
|
|
313
|
+
...producerSurvey,
|
|
314
|
+
candidateSetId: candidateSet.id,
|
|
315
|
+
candidateSetStatus: candidateSet.status,
|
|
316
|
+
// No candidate is selected; every value the set held, in set order.
|
|
317
|
+
candidates: candidateSet.candidates.map((candidate) => ({
|
|
318
|
+
candidateId: candidate.id,
|
|
319
|
+
value: candidate.value,
|
|
320
|
+
...(candidate.rejectionReason !== undefined ? { rejectionReason: candidate.rejectionReason } : {}),
|
|
321
|
+
})),
|
|
322
|
+
reviewOutcomeId: review?.id,
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
});
|
|
326
|
+
context.events.push({
|
|
327
|
+
id: projectionRecordId(projection.id, projectionContextId, "claim-event", `event.${status}`),
|
|
328
|
+
claimId: projection.id,
|
|
329
|
+
status,
|
|
330
|
+
actor: projection.actor ?? (review?.resolution === "could_not_confirm" ? undefined : review?.actor) ?? projection.collectedBy,
|
|
331
|
+
method: projection.eventMethod ?? eventMethodFor(status, candidateSet),
|
|
332
|
+
evidenceIds,
|
|
333
|
+
createdAt: review?.reviewedAt ?? input.generatedAt,
|
|
334
|
+
notes: review?.rationale ?? candidateSet.rationale,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
223
337
|
function validateProjectionContextId(contextId) {
|
|
224
338
|
if (contextId === undefined)
|
|
225
339
|
return undefined;
|
|
@@ -504,6 +618,8 @@ function statusFor(input) {
|
|
|
504
618
|
return "disputed";
|
|
505
619
|
if (input.candidateSet.status === "escalated")
|
|
506
620
|
return "disputed";
|
|
621
|
+
if (input.candidateSet.status === "rejected")
|
|
622
|
+
return "rejected";
|
|
507
623
|
return "proposed";
|
|
508
624
|
}
|
|
509
625
|
function assertProducerDiscipline(input) {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -28,7 +28,11 @@ export interface Extraction {
|
|
|
28
28
|
extractedAt: string;
|
|
29
29
|
metadata?: Record<string, unknown>;
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* `rejected`: a review rejected every candidate in the set and selected none
|
|
33
|
+
* (for example rejecting all conflicting values for one claim).
|
|
34
|
+
*/
|
|
35
|
+
export type CandidateSetStatus = "resolved" | "needs-review" | "conflict" | "escalated" | "rejected";
|
|
32
36
|
export type EscalationDimension = "framing" | "completeness" | "conclusion" | "citation";
|
|
33
37
|
export interface Candidate {
|
|
34
38
|
id: string;
|
package/package.json
CHANGED