@kontourai/survey 3.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/README.md +4 -0
- package/dist/examples/calibrated-auto-accept.d.ts +22 -15
- package/dist/examples/calibrated-auto-accept.js +40 -36
- package/dist/examples/review-workbench/server-apply-consumer.js +5 -0
- package/dist/src/calibration.d.ts +48 -21
- package/dist/src/calibration.js +72 -33
- package/dist/src/canonical-reviewed-trust-input.js +73 -34
- package/dist/src/console/review-console-server.d.ts +3 -1
- package/dist/src/console/review-console-server.js +203 -50
- package/dist/src/extraction-envelope.d.ts +81 -3
- package/dist/src/extraction-envelope.js +183 -24
- package/dist/src/index.d.ts +9 -8
- package/dist/src/index.js +3 -3
- package/dist/src/inquiry-mapping.d.ts +15 -1
- package/dist/src/inquiry-mapping.js +10 -2
- package/dist/src/mcp/review-mcp.js +112 -90
- package/dist/src/producer-profile.d.ts +41 -2
- package/dist/src/producer-profile.js +29 -2
- package/dist/src/review-session-file.d.ts +64 -0
- package/dist/src/review-session-file.js +320 -0
- package/dist/src/review-workbench/edited-value.d.ts +70 -0
- package/dist/src/review-workbench/edited-value.js +147 -0
- 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 +46 -1
- package/dist/src/review-workbench/review-presentation.js +72 -1
- package/dist/src/review-workbench/review-queue-session.d.ts +19 -5
- package/dist/src/review-workbench/review-queue-session.js +54 -14
- package/dist/src/review-workbench/review-session-replay.d.ts +35 -1
- package/dist/src/review-workbench/review-session-replay.js +82 -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 +22 -11
- package/dist/src/review-workbench/review-workbench.js +91 -26
- package/dist/src/review-workbench/review-workbench.standalone.css +2 -0
- package/dist/src/review-workbench/server-review-session.d.ts +3 -1
- package/dist/src/review-workbench/server-review-session.js +1 -0
- package/dist/src/reviewed-candidate-resolution.js +13 -7
- package/dist/src/schema-mapping.d.ts +23 -0
- package/dist/src/schema-mapping.js +30 -20
- package/dist/src/surface-reviewed-extraction.js +4 -0
- package/dist/src/to-surface.d.ts +30 -6
- package/dist/src/to-surface.js +312 -18
- package/dist/src/types.d.ts +44 -1
- package/package.json +5 -4
|
@@ -244,6 +244,8 @@
|
|
|
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
|
.survey-workbench-embed .inspector-posture.digest-mismatch, .survey-workbench-embed .inspector-posture.artifact-unavailable, .survey-workbench-embed .inspector-posture.excerpt-mismatch{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
247
|
+
.survey-workbench-embed .inspector-posture.extraction-failed{ background: var(--k-negative-wash); color: var(--k-negative); border: 2px solid currentColor; }
|
|
248
|
+
.survey-workbench-embed .inspector-posture.extraction-incomplete{ background: var(--k-caution-wash); color: var(--k-caution); border: 2px solid currentColor; }
|
|
247
249
|
.survey-workbench-embed .inspector-filters{ display: flex; flex-wrap: wrap; gap: .6rem; margin: 1rem 0; }
|
|
248
250
|
.survey-workbench-embed .inspector-filters label{ display: grid; gap: .25rem; font-size: .75rem; color: var(--k-text-muted); }
|
|
249
251
|
.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; }
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { type ReviewQueueSessionState, type ReviewWorkbenchDecision, type ReviewWorkbenchState } from "./review-queue-session.js";
|
|
2
|
-
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
2
|
+
import { type ReviewSessionReplayIssue, type ReviewSessionReplayWarning } from "./review-session-replay.js";
|
|
3
3
|
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
4
4
|
import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewValueDescriptor } from "../review-resource.js";
|
|
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
|
-
export { validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, } from "./review-session-replay.js";
|
|
11
|
+
export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, type ReviewSessionReplayWarning, } from "./review-session-replay.js";
|
|
12
|
+
export { checkEditedValueForItem, editedValueFromEditorText, type EditedValueCheck, } from "./edited-value.js";
|
|
12
13
|
export { assertReviewDecisionModeAllows, DecisionModeViolationError, validateReviewDecisionMode, type ReviewDecisionModeIssue, type ReviewDecisionModeIssueCode, type ReviewDecisionModeResult, } from "./producer-decision-mode.js";
|
|
13
14
|
export declare function buildReviewDecision(state: ReviewWorkbenchState): ReviewDecision | undefined;
|
|
14
15
|
export interface ReviewWorkbenchSessionExport {
|
|
@@ -34,6 +35,8 @@ export interface DeriveReviewSessionApplyResultForSnapshotOptions {
|
|
|
34
35
|
export type DeriveReviewSessionApplyResultForSnapshotResult = {
|
|
35
36
|
readonly ok: true;
|
|
36
37
|
readonly issues: readonly [];
|
|
38
|
+
/** Non-fatal replay notes, e.g. a legacy text edit converted to its typed value. */
|
|
39
|
+
readonly warnings?: readonly ReviewSessionReplayWarning[];
|
|
37
40
|
readonly unresolvedItemNames: readonly string[];
|
|
38
41
|
readonly replayedSession: ReviewQueueSessionState;
|
|
39
42
|
readonly sessionExport: ReviewWorkbenchSessionExport;
|
|
@@ -42,6 +45,7 @@ export type DeriveReviewSessionApplyResultForSnapshotResult = {
|
|
|
42
45
|
} | {
|
|
43
46
|
readonly ok: false;
|
|
44
47
|
readonly issues: readonly ReviewSessionApplyIssue[];
|
|
48
|
+
readonly warnings?: readonly ReviewSessionReplayWarning[];
|
|
45
49
|
readonly unresolvedItemNames: readonly string[];
|
|
46
50
|
readonly replayedSession?: ReviewQueueSessionState;
|
|
47
51
|
readonly sessionExport?: ReviewWorkbenchSessionExport;
|
|
@@ -106,24 +110,31 @@ export interface ReviewQueueWindow {
|
|
|
106
110
|
export interface BrowserReviewWorkbenchConfig {
|
|
107
111
|
readonly startState?: ReviewQueueSessionState | ReviewWorkbenchState;
|
|
108
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
|
+
*/
|
|
109
120
|
export interface ReviewWorkbenchResult {
|
|
110
121
|
readonly reviewItemName: string;
|
|
111
122
|
readonly decision: ReviewWorkbenchDecision;
|
|
112
|
-
readonly selectedCandidate
|
|
113
|
-
readonly selectedCandidateId
|
|
123
|
+
readonly selectedCandidate?: ReviewCandidate;
|
|
124
|
+
readonly selectedCandidateId?: string;
|
|
114
125
|
readonly selectedCandidateRole?: ReviewCandidate["role"];
|
|
115
126
|
/** The selected candidate's original value, unaffected by any reviewer edit. Used
|
|
116
127
|
* for candidate-identity matching (see `matchingSelectedCandidate`); consumers who
|
|
117
128
|
* want the reviewer's edited value should read `effectiveValue` instead. */
|
|
118
|
-
readonly selectedValue
|
|
119
|
-
readonly selectedDisplayValue
|
|
129
|
+
readonly selectedValue?: unknown;
|
|
130
|
+
readonly selectedDisplayValue?: string;
|
|
120
131
|
/** Reviewer-edited override captured for an accept-proposed decision, if the
|
|
121
132
|
* reviewer changed the proposed value before applying it. Additive/optional. */
|
|
122
133
|
readonly editedValue?: unknown;
|
|
123
134
|
/** The value that should actually be applied: `editedValue` when present (and the
|
|
124
135
|
* decision selects the proposed candidate), otherwise `selectedValue`. */
|
|
125
|
-
readonly effectiveValue
|
|
126
|
-
readonly effectiveDisplayValue
|
|
136
|
+
readonly effectiveValue?: unknown;
|
|
137
|
+
readonly effectiveDisplayValue?: string;
|
|
127
138
|
readonly unselectedCandidates: readonly ReviewCandidate[];
|
|
128
139
|
readonly reviewDecision: ReviewDecision;
|
|
129
140
|
readonly status: ReviewDecision["spec"]["status"];
|
|
@@ -207,8 +218,8 @@ export declare function buildReviewQueueWindow(session: ReviewQueueSessionState,
|
|
|
207
218
|
* error message when the value violates the declared type/enum constraint, or
|
|
208
219
|
* `undefined` when it is acceptable — including when there is no descriptor or
|
|
209
220
|
* the type carries no single-line constraint (string/array/object). This is a
|
|
210
|
-
* FORMAT check
|
|
211
|
-
*
|
|
221
|
+
* FORMAT check on the editor text; on accept the workbench stores the text
|
|
222
|
+
* converted to the descriptor's JSON type (`editedValueFromEditorText`).
|
|
212
223
|
*/
|
|
213
224
|
export declare function validateProposedValue(descriptor: ReviewValueDescriptor | undefined, rawValue: string): string | undefined;
|
|
214
225
|
export declare function mountReviewWorkbench(root: HTMLElement, startState?: ReviewQueueSessionState | ReviewWorkbenchState, options?: MountReviewWorkbenchOptions): void;
|
|
@@ -1,20 +1,22 @@
|
|
|
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";
|
|
4
|
-
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
3
|
+
import { candidateForDecision, decisionCandidateId, decisionSelectsNoCandidate, keepActionDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
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
9
|
import { humanizeIdentifier } from "./review-presentation.js";
|
|
10
|
+
import { editedValueFromEditorText, isIsoCalendarDate, parsePlainDecimal } from "./edited-value.js";
|
|
10
11
|
import { createAuditFactTrace, } from "./audit-rows.js";
|
|
11
12
|
export { reviewAuditRowKeys } from "./audit-rows.js";
|
|
12
13
|
export { assertReviewQueueAgainstExtractionImport, assertReviewQueueBinding, bindReviewQueue, hashReviewQueueSnapshot, UnattestedExtractionQueueError, UnattestedReviewQueueError, validateReviewQueueAgainstExtractionImport, validateReviewQueueBinding, } from "./queue-binding.js";
|
|
13
14
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
14
|
-
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";
|
|
15
16
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
16
17
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
17
|
-
export { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
18
|
+
export { reviewSessionReplayWarningsForSnapshot, validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
19
|
+
export { checkEditedValueForItem, editedValueFromEditorText, } from "./edited-value.js";
|
|
18
20
|
export { assertReviewDecisionModeAllows, DecisionModeViolationError, validateReviewDecisionMode, } from "./producer-decision-mode.js";
|
|
19
21
|
export function buildReviewDecision(state) {
|
|
20
22
|
if (!state.decision) {
|
|
@@ -25,11 +27,15 @@ export function buildReviewDecision(state) {
|
|
|
25
27
|
throw new Error("Could not confirm requires a non-empty reason.");
|
|
26
28
|
}
|
|
27
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);
|
|
28
33
|
const projection = {
|
|
29
|
-
...candidate.projection,
|
|
30
|
-
reviewOutcomeId: candidate.projection?.reviewOutcomeId
|
|
34
|
+
...(selectsNone ? state.item.spec.projection : candidate.projection),
|
|
35
|
+
reviewOutcomeId: (selectsNone ? state.item.spec.projection : candidate.projection)?.reviewOutcomeId
|
|
31
36
|
?? `${state.item.metadata.name}:${state.decision}:review-outcome`,
|
|
32
37
|
};
|
|
38
|
+
const candidateProjection = selectsNone ? undefined : candidate.projection;
|
|
33
39
|
const authorizing = buildDecisionCardAuthorizing(state);
|
|
34
40
|
const reviewDecision = {
|
|
35
41
|
apiVersion: reviewResourceApiVersion,
|
|
@@ -41,7 +47,7 @@ export function buildReviewDecision(state) {
|
|
|
41
47
|
},
|
|
42
48
|
spec: {
|
|
43
49
|
reviewItemName: state.item.metadata.name,
|
|
44
|
-
candidateId: candidate.id,
|
|
50
|
+
...(selectsNone ? {} : { candidateId: candidate.id }),
|
|
45
51
|
status: definition.status,
|
|
46
52
|
...(state.decision === "could-not-confirm"
|
|
47
53
|
? {
|
|
@@ -62,7 +68,7 @@ export function buildReviewDecision(state) {
|
|
|
62
68
|
: {}),
|
|
63
69
|
},
|
|
64
70
|
status: {
|
|
65
|
-
...(
|
|
71
|
+
...(candidateProjection?.claimId ? { appliedToClaimIds: [candidateProjection.claimId] } : {}),
|
|
66
72
|
},
|
|
67
73
|
};
|
|
68
74
|
assertReviewResolutionConsistency(`ReviewDecision ${reviewDecision.metadata.name}`, {
|
|
@@ -124,7 +130,16 @@ function buildDecisionCardAuthorizing(state) {
|
|
|
124
130
|
*/
|
|
125
131
|
function decisionCardRenderedPrompt(state, targetLabel) {
|
|
126
132
|
const currentCandidate = state.item.spec.candidates.find((c) => c.role === "current");
|
|
127
|
-
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];
|
|
128
143
|
const currentValue = formatValue(currentCandidate?.value ?? "");
|
|
129
144
|
const proposedValue = formatValue(proposedCandidate?.value ?? "");
|
|
130
145
|
const decisionLabel = state.decision ? workbenchDecisionDefinitions[state.decision].label : "";
|
|
@@ -204,7 +219,9 @@ function duplicateTargetIssues(items) {
|
|
|
204
219
|
return issues;
|
|
205
220
|
}
|
|
206
221
|
function mapReviewApplyResultToActions(input) {
|
|
207
|
-
|
|
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) {
|
|
208
225
|
return [];
|
|
209
226
|
}
|
|
210
227
|
const context = buildReviewApplyActionContext(input.result, input.itemByName, input.issues);
|
|
@@ -254,6 +271,8 @@ function buildReviewApplyActionContext(result, itemByName, issues) {
|
|
|
254
271
|
};
|
|
255
272
|
}
|
|
256
273
|
function matchingSelectedCandidate(item, result) {
|
|
274
|
+
if (result.selectedCandidateId === undefined)
|
|
275
|
+
return undefined;
|
|
257
276
|
const candidate = findSoleCandidateById(item, result.selectedCandidateId);
|
|
258
277
|
return candidate
|
|
259
278
|
&& candidate.role === result.selectedCandidateRole
|
|
@@ -278,6 +297,16 @@ export function buildReviewWorkbenchResultsFromSession(session) {
|
|
|
278
297
|
if (!reviewDecision) {
|
|
279
298
|
return [];
|
|
280
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
|
+
}
|
|
281
310
|
const selectedCandidate = candidateForDecision(item, decision);
|
|
282
311
|
const editedValue = decision === "accept-proposed"
|
|
283
312
|
? session.editedValuesByItemName?.[item.metadata.name]
|
|
@@ -332,6 +361,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
|
|
|
332
361
|
results: [],
|
|
333
362
|
};
|
|
334
363
|
}
|
|
364
|
+
const warnings = reviewSessionReplayWarningsForSnapshot(options.snapshot, options.events);
|
|
335
365
|
const replayedSession = replayReviewSessionEvents(options.snapshot, options.events);
|
|
336
366
|
const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events);
|
|
337
367
|
const resolvedItemNames = new Set(sessionExport.results.map((result) => result.reviewItemName));
|
|
@@ -356,6 +386,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
|
|
|
356
386
|
return {
|
|
357
387
|
ok: false,
|
|
358
388
|
issues,
|
|
389
|
+
warnings,
|
|
359
390
|
unresolvedItemNames,
|
|
360
391
|
replayedSession,
|
|
361
392
|
sessionExport,
|
|
@@ -366,6 +397,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
|
|
|
366
397
|
return {
|
|
367
398
|
ok: true,
|
|
368
399
|
issues: [],
|
|
400
|
+
warnings,
|
|
369
401
|
unresolvedItemNames,
|
|
370
402
|
replayedSession,
|
|
371
403
|
sessionExport,
|
|
@@ -486,13 +518,13 @@ function fieldCardState(item, decision) {
|
|
|
486
518
|
* {@link keepActionDecision}). Saying "Kept — flagged wrong" there would claim a
|
|
487
519
|
* distinction the record does not hold, so the chip states only what happened.
|
|
488
520
|
*/
|
|
489
|
-
function chipLabel(state, hasCurrentValue) {
|
|
521
|
+
function chipLabel(state, hasCurrentValue, conflictingValues) {
|
|
490
522
|
switch (state) {
|
|
491
523
|
case "accepted": return "Accepted";
|
|
492
|
-
case "kept": return hasCurrentValue ? "Kept current" : "Left unset";
|
|
493
|
-
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";
|
|
494
526
|
case "could-not-confirm": return "Could not confirm";
|
|
495
|
-
default: return "Needs review";
|
|
527
|
+
default: return conflictingValues ? `Conflict: ${conflictingValues} values` : "Needs review";
|
|
496
528
|
}
|
|
497
529
|
}
|
|
498
530
|
const DEFAULT_REVIEW_QUEUE_PAGE_SIZE = 50;
|
|
@@ -613,11 +645,16 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
613
645
|
const state = fieldCardState(item, decision);
|
|
614
646
|
const decided = decision !== undefined;
|
|
615
647
|
const current = item.spec.candidates.find((candidate) => candidate.role === "current");
|
|
616
|
-
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;
|
|
617
654
|
const presentation = buildReviewItemPresentation(item, presentationAdapter);
|
|
618
655
|
const hasCurrentValue = current !== undefined && !isEmptyValue(current.value);
|
|
619
656
|
const kind = hasCurrentValue ? "Update" : "New";
|
|
620
|
-
const keepLabel = hasCurrentValue ? "Keep current" : "Leave unset";
|
|
657
|
+
const keepLabel = hasCurrentValue ? "Keep current" : conflict ? "Reject all values" : "Leave unset";
|
|
621
658
|
// Only render controls whose decision this item can actually record. Every
|
|
622
659
|
// decision resolves to a candidate role, so a control routed at a role the
|
|
623
660
|
// item does not carry cannot emit anything — it used to throw mid-click and
|
|
@@ -648,11 +685,13 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
648
685
|
<div class="frow1">
|
|
649
686
|
<span class="fname">${escapeHtml(presentation.targetLabel)}</span>
|
|
650
687
|
<span class="fkind">${kind}</span>
|
|
651
|
-
<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>
|
|
652
689
|
</div>
|
|
653
690
|
${proposed
|
|
654
691
|
? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
|
|
655
|
-
:
|
|
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>"}
|
|
656
695
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
657
696
|
<div class="decide">
|
|
658
697
|
${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
|
|
@@ -662,6 +701,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
662
701
|
Suggestion was wrong
|
|
663
702
|
</label>
|
|
664
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>` : ""}
|
|
665
705
|
</div>
|
|
666
706
|
<!--
|
|
667
707
|
A control's precondition message belongs where the control is. The
|
|
@@ -672,7 +712,7 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
672
712
|
-->
|
|
673
713
|
<span class="derr" data-testid="decision-error" role="alert" hidden></span>
|
|
674
714
|
<div class="decided">
|
|
675
|
-
<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>
|
|
676
716
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
677
717
|
</div>
|
|
678
718
|
${renderAuditDetails(item, current, proposed, session, presentationAdapter)}
|
|
@@ -680,6 +720,24 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
680
720
|
</section>
|
|
681
721
|
`;
|
|
682
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
|
+
}
|
|
683
741
|
/**
|
|
684
742
|
* Renders the Current → Proposed diff row. The proposed side is editable
|
|
685
743
|
* (via {@link renderProposedValueEditor}) until the field has a decision.
|
|
@@ -804,8 +862,8 @@ function toDateInputValue(text) {
|
|
|
804
862
|
* error message when the value violates the declared type/enum constraint, or
|
|
805
863
|
* `undefined` when it is acceptable — including when there is no descriptor or
|
|
806
864
|
* the type carries no single-line constraint (string/array/object). This is a
|
|
807
|
-
* FORMAT check
|
|
808
|
-
*
|
|
865
|
+
* FORMAT check on the editor text; on accept the workbench stores the text
|
|
866
|
+
* converted to the descriptor's JSON type (`editedValueFromEditorText`).
|
|
809
867
|
*/
|
|
810
868
|
export function validateProposedValue(descriptor, rawValue) {
|
|
811
869
|
if (!descriptor)
|
|
@@ -815,7 +873,9 @@ export function validateProposedValue(descriptor, rawValue) {
|
|
|
815
873
|
case "number":
|
|
816
874
|
if (value === "")
|
|
817
875
|
return "Enter a number.";
|
|
818
|
-
return
|
|
876
|
+
return parsePlainDecimal(value) !== undefined
|
|
877
|
+
? undefined
|
|
878
|
+
: `"${rawValue}" is not a number (use plain decimal digits: any safe integer, or a fraction of at most 15 significant digits).`;
|
|
819
879
|
case "boolean":
|
|
820
880
|
if (value === "")
|
|
821
881
|
return "Choose true or false.";
|
|
@@ -823,7 +883,7 @@ export function validateProposedValue(descriptor, rawValue) {
|
|
|
823
883
|
case "date":
|
|
824
884
|
if (value === "")
|
|
825
885
|
return "Enter a date.";
|
|
826
|
-
return
|
|
886
|
+
return isIsoCalendarDate(value)
|
|
827
887
|
? undefined
|
|
828
888
|
: `"${rawValue}" is not a valid date (YYYY-MM-DD).`;
|
|
829
889
|
case "enum": {
|
|
@@ -1158,7 +1218,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1158
1218
|
const session = sessionForEvent;
|
|
1159
1219
|
const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
|
|
1160
1220
|
const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
|
|
1161
|
-
const
|
|
1221
|
+
const candidateId = item && decision ? decisionCandidateId(item, decision) : undefined;
|
|
1162
1222
|
const definition = decision ? workbenchDecisionDefinitions[decision] : undefined;
|
|
1163
1223
|
const note = itemName ? session.notesByItemName[itemName] : undefined;
|
|
1164
1224
|
// Carry the reviewer's inline edit in the event (accept-proposed only), so
|
|
@@ -1181,7 +1241,7 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1181
1241
|
occurredAt: session.reviewedAt,
|
|
1182
1242
|
reviewItemName: itemName,
|
|
1183
1243
|
reviewDecisionName: item && decision ? `${item.metadata.name}-${decision}` : undefined,
|
|
1184
|
-
candidateId
|
|
1244
|
+
candidateId,
|
|
1185
1245
|
status: definition?.status,
|
|
1186
1246
|
...(decision === "could-not-confirm"
|
|
1187
1247
|
? {
|
|
@@ -1210,7 +1270,12 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1210
1270
|
const originalText = proposed ? formatValue(proposed.value) : undefined;
|
|
1211
1271
|
const nextEditedValuesByItemName = { ...session.editedValuesByItemName };
|
|
1212
1272
|
if (decision === "accept-proposed" && rawEditedValue !== undefined && rawEditedValue !== originalText) {
|
|
1213
|
-
|
|
1273
|
+
// Store the edit as the descriptor's JSON type (a number field's "42" is
|
|
1274
|
+
// 42), so the decision event and effectiveValue carry a typed value the
|
|
1275
|
+
// server apply boundary accepts (kontourai/survey#278). Text that does not
|
|
1276
|
+
// parse is kept as typed; the apply boundary refuses it.
|
|
1277
|
+
const typedValue = editedValueFromEditorText(item?.spec.valueDescriptor, rawEditedValue);
|
|
1278
|
+
nextEditedValuesByItemName[itemName] = typedValue === undefined ? rawEditedValue : typedValue;
|
|
1214
1279
|
}
|
|
1215
1280
|
else {
|
|
1216
1281
|
delete nextEditedValuesByItemName[itemName];
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
import { type DeriveReviewSessionApplyResultForSnapshotResult, type MapReviewWorkbenchResultsToApplyActionsOptions, type ReviewApplyActionIssue, type ReviewApplyActionMapping, type ReviewSessionApplyIssue, type ReviewSessionApplyResolutionRequirement, type ReviewWorkbenchResult } from "./review-workbench.js";
|
|
2
2
|
import { type ReviewDecisionModeIssue } from "./producer-decision-mode.js";
|
|
3
|
-
import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
3
|
+
import { type ReviewSessionReplayIssue, type ReviewSessionReplayWarning } from "./review-session-replay.js";
|
|
4
4
|
import type { ReviewDecision, ReviewSessionEvent } from "../review-resource.js";
|
|
5
5
|
import { type ReviewQueueBinding } from "./queue-binding.js";
|
|
6
6
|
import { type ReviewQueueSessionState } from "./review-queue-session.js";
|
|
@@ -128,6 +128,8 @@ export type ApplyReviewSessionIssue = ReviewSessionApplyIssue | {
|
|
|
128
128
|
export type ApplyReviewSessionResult<TAction = never> = {
|
|
129
129
|
readonly ok: true;
|
|
130
130
|
readonly issues: readonly [];
|
|
131
|
+
/** Non-fatal replay notes, e.g. a legacy text edit converted to its typed value. */
|
|
132
|
+
readonly warnings?: readonly ReviewSessionReplayWarning[];
|
|
131
133
|
readonly decisions: readonly ReviewDecision[];
|
|
132
134
|
readonly results: readonly ReviewWorkbenchResult[];
|
|
133
135
|
readonly actions: readonly ReviewApplyActionMapping<TAction>[];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { candidateReviewRecord } from "./builder.js";
|
|
2
|
+
import { ReviewAgreementError } from "./to-surface.js";
|
|
2
3
|
export function reviewedCandidateResolution(input) {
|
|
3
4
|
return candidateReviewRecord({
|
|
4
5
|
id: input.id,
|
|
@@ -11,13 +12,12 @@ export function reviewedCandidateResolution(input) {
|
|
|
11
12
|
...input.reviewOutcome,
|
|
12
13
|
candidateId: input.reviewOutcome.candidateId ?? input.selectedCandidateId,
|
|
13
14
|
},
|
|
14
|
-
observations: input.observations.map((observation) =>
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
})),
|
|
15
|
+
observations: input.observations.map((observation) => {
|
|
16
|
+
const status = observation.claim.status ?? claimStatusForObservation(input, observation);
|
|
17
|
+
if (observationCandidateId(observation) === input.selectedCandidateId)
|
|
18
|
+
assertSelectedStatusAgrees(input, status);
|
|
19
|
+
return { ...observation, claim: { ...observation.claim, status } };
|
|
20
|
+
}),
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
function claimStatusForObservation(input, observation) {
|
|
@@ -26,6 +26,12 @@ function claimStatusForObservation(input, observation) {
|
|
|
26
26
|
}
|
|
27
27
|
return input.unselectedClaimStatus ?? "superseded";
|
|
28
28
|
}
|
|
29
|
+
/** A trusted selected claim must carry the status its review decided. */
|
|
30
|
+
function assertSelectedStatusAgrees(input, status) {
|
|
31
|
+
if ((status === "verified" || status === "assumed") && status !== input.reviewOutcome.status) {
|
|
32
|
+
throw new ReviewAgreementError("status-mismatch", `Candidate set ${input.id} selected claim status ${status} disagrees with review outcome status ${input.reviewOutcome.status}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
function observationCandidateId(observation) {
|
|
30
36
|
return observation.candidate?.id ?? `${observation.id}.candidate`;
|
|
31
37
|
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* capping.
|
|
22
22
|
*/
|
|
23
23
|
import type { TrustBundle } from "@kontourai/surface";
|
|
24
|
+
import type { AutoAcceptWarning } from "./producer-profile.js";
|
|
24
25
|
import type { Candidate, CandidateSet, ReviewOutcome, SurveyInput } from "./types.js";
|
|
25
26
|
/**
|
|
26
27
|
* A stable reference to one field within one system's schema.
|
|
@@ -109,6 +110,10 @@ export interface SchemaMappingOptions {
|
|
|
109
110
|
* If set, proposals at or above this confidence threshold are auto-accepted
|
|
110
111
|
* as "assumed" (mirrors applyAutoAcceptPolicy in inquiry-mapping).
|
|
111
112
|
* Conflicting proposals are never auto-accepted.
|
|
113
|
+
*
|
|
114
|
+
* Experimental: the gate trusts the proposer's self-reported confidence.
|
|
115
|
+
* Must be a finite number in (0, 1] (otherwise `RangeError`); a proposal
|
|
116
|
+
* whose confidence is not a finite number in [0, 1] is never auto-accepted.
|
|
112
117
|
*/
|
|
113
118
|
autoAcceptMinConfidence?: number;
|
|
114
119
|
/** ISO 8601 timestamp; defaults to new Date().toISOString(). */
|
|
@@ -116,6 +121,18 @@ export interface SchemaMappingOptions {
|
|
|
116
121
|
/** Identifies the Survey producer run. */
|
|
117
122
|
source?: string;
|
|
118
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* The schema-mapping profile's own payload, carried verbatim under the core's
|
|
126
|
+
* canonical `Candidate.metadata` key (see `./producer-profile.js`). Covers
|
|
127
|
+
* every field read across `mappingReviewToSurface`'s read-back sites and
|
|
128
|
+
* written at Candidate-projection time below.
|
|
129
|
+
*/
|
|
130
|
+
/**
|
|
131
|
+
* The value a schema-mapping candidate and its claim carry: the whole mapping,
|
|
132
|
+
* source field included, so a reviewed claim's value is exactly the reviewed
|
|
133
|
+
* candidate value.
|
|
134
|
+
*/
|
|
135
|
+
export type SchemaMappingValue = Pick<MappingProposalRecord, "relation" | "sourceField" | "targetField" | "conversion">;
|
|
119
136
|
/**
|
|
120
137
|
* Run the extractor against the provided system schemas and project the
|
|
121
138
|
* resulting proposals into the standard Survey chain:
|
|
@@ -142,6 +159,12 @@ export declare function surveySchemaMapping(context: {
|
|
|
142
159
|
surveyInput: SurveyInput;
|
|
143
160
|
proposals: MappingProposalRecord[];
|
|
144
161
|
candidateSets: CandidateSet[];
|
|
162
|
+
/**
|
|
163
|
+
* Proposals the auto-accept policy refused because their confidence is not
|
|
164
|
+
* a finite number in [0, 1]. Present only when non-empty; those proposals
|
|
165
|
+
* stay in human review.
|
|
166
|
+
*/
|
|
167
|
+
autoAcceptWarnings?: AutoAcceptWarning[];
|
|
145
168
|
}>;
|
|
146
169
|
/**
|
|
147
170
|
* A reviewed (accepted or rejected) mapping record: the CandidateSet, the
|