@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.
Files changed (46) hide show
  1. package/README.md +4 -0
  2. package/dist/examples/calibrated-auto-accept.d.ts +22 -15
  3. package/dist/examples/calibrated-auto-accept.js +40 -36
  4. package/dist/examples/review-workbench/server-apply-consumer.js +5 -0
  5. package/dist/src/calibration.d.ts +48 -21
  6. package/dist/src/calibration.js +72 -33
  7. package/dist/src/canonical-reviewed-trust-input.js +73 -34
  8. package/dist/src/console/review-console-server.d.ts +3 -1
  9. package/dist/src/console/review-console-server.js +203 -50
  10. package/dist/src/extraction-envelope.d.ts +81 -3
  11. package/dist/src/extraction-envelope.js +183 -24
  12. package/dist/src/index.d.ts +9 -8
  13. package/dist/src/index.js +3 -3
  14. package/dist/src/inquiry-mapping.d.ts +15 -1
  15. package/dist/src/inquiry-mapping.js +10 -2
  16. package/dist/src/mcp/review-mcp.js +112 -90
  17. package/dist/src/producer-profile.d.ts +41 -2
  18. package/dist/src/producer-profile.js +29 -2
  19. package/dist/src/review-session-file.d.ts +64 -0
  20. package/dist/src/review-session-file.js +320 -0
  21. package/dist/src/review-workbench/edited-value.d.ts +70 -0
  22. package/dist/src/review-workbench/edited-value.js +147 -0
  23. package/dist/src/review-workbench/extraction-inspector.d.ts +15 -1
  24. package/dist/src/review-workbench/extraction-inspector.js +55 -19
  25. package/dist/src/review-workbench/queue-binding.js +1 -1
  26. package/dist/src/review-workbench/review-presentation.d.ts +46 -1
  27. package/dist/src/review-workbench/review-presentation.js +72 -1
  28. package/dist/src/review-workbench/review-queue-session.d.ts +19 -5
  29. package/dist/src/review-workbench/review-queue-session.js +54 -14
  30. package/dist/src/review-workbench/review-session-replay.d.ts +35 -1
  31. package/dist/src/review-workbench/review-session-replay.js +82 -3
  32. package/dist/src/review-workbench/review-workbench-css.generated.js +2 -0
  33. package/dist/src/review-workbench/review-workbench.css +2 -0
  34. package/dist/src/review-workbench/review-workbench.d.ts +22 -11
  35. package/dist/src/review-workbench/review-workbench.js +91 -26
  36. package/dist/src/review-workbench/review-workbench.standalone.css +2 -0
  37. package/dist/src/review-workbench/server-review-session.d.ts +3 -1
  38. package/dist/src/review-workbench/server-review-session.js +1 -0
  39. package/dist/src/reviewed-candidate-resolution.js +13 -7
  40. package/dist/src/schema-mapping.d.ts +23 -0
  41. package/dist/src/schema-mapping.js +30 -20
  42. package/dist/src/surface-reviewed-extraction.js +4 -0
  43. package/dist/src/to-surface.d.ts +30 -6
  44. package/dist/src/to-surface.js +312 -18
  45. package/dist/src/types.d.ts +44 -1
  46. 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: ReviewCandidate;
113
- readonly selectedCandidateId: string;
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: unknown;
119
- readonly selectedDisplayValue: string;
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: unknown;
126
- readonly effectiveDisplayValue: string;
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 only: it never coerces or rewrites the value (the workbench
211
- * stores the reviewer's string edit unchanged, as it did before typed editors).
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
- ...(candidate.projection?.claimId ? { appliedToClaimIds: [candidate.projection.claimId] } : {}),
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 proposedCandidate = state.item.spec.candidates.find((c) => c.role === "proposed");
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
- if (input.result.decision === "could-not-confirm") {
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 proposed = item.spec.candidates.find((candidate) => candidate.role === "proposed");
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
- : "<p class=\"field-value\">No proposed value is available for this field.</p>"}
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 only: it never coerces or rewrites the value (the workbench
808
- * stores the reviewer's string edit unchanged, as it did before typed editors).
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 Number.isFinite(Number(value)) ? undefined : `"${rawValue}" is not a number.`;
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 /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value))
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 candidate = item && decision ? candidateForDecision(item, decision) : undefined;
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: candidate?.id,
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
- nextEditedValuesByItemName[itemName] = rawEditedValue;
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>[];
@@ -225,6 +225,7 @@ export function applyReviewSession(options) {
225
225
  return {
226
226
  ok: true,
227
227
  issues: [],
228
+ warnings: derived.warnings ?? [],
228
229
  decisions: derived.decisions,
229
230
  results: derived.results,
230
231
  actions,
@@ -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
- ...observation,
16
- claim: {
17
- ...observation.claim,
18
- status: observation.claim.status ?? claimStatusForObservation(input, observation),
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