@kontourai/survey 3.0.0 → 4.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 (37) hide show
  1. package/README.md +4 -0
  2. package/dist/example-data/public-directory-review-resource.d.ts +3 -3
  3. package/dist/examples/calibrated-auto-accept.d.ts +22 -15
  4. package/dist/examples/calibrated-auto-accept.js +40 -36
  5. package/dist/src/calibration.d.ts +48 -21
  6. package/dist/src/calibration.js +72 -33
  7. package/dist/src/console/review-console-server.d.ts +3 -1
  8. package/dist/src/console/review-console-server.js +203 -50
  9. package/dist/src/extraction-envelope.d.ts +22 -0
  10. package/dist/src/extraction-envelope.js +25 -4
  11. package/dist/src/index.d.ts +8 -7
  12. package/dist/src/index.js +2 -2
  13. package/dist/src/inquiry-mapping.d.ts +15 -1
  14. package/dist/src/inquiry-mapping.js +10 -2
  15. package/dist/src/mcp/review-mcp.js +70 -65
  16. package/dist/src/producer-profile.d.ts +41 -2
  17. package/dist/src/producer-profile.js +29 -2
  18. package/dist/src/review-session-file.d.ts +64 -0
  19. package/dist/src/review-session-file.js +320 -0
  20. package/dist/src/review-workbench/edited-value.d.ts +70 -0
  21. package/dist/src/review-workbench/edited-value.js +147 -0
  22. package/dist/src/review-workbench/review-presentation.d.ts +44 -0
  23. package/dist/src/review-workbench/review-presentation.js +49 -0
  24. package/dist/src/review-workbench/review-queue-session.js +8 -1
  25. package/dist/src/review-workbench/review-session-replay.d.ts +35 -1
  26. package/dist/src/review-workbench/review-session-replay.js +77 -0
  27. package/dist/src/review-workbench/review-workbench.d.ts +8 -4
  28. package/dist/src/review-workbench/review-workbench.js +19 -7
  29. package/dist/src/review-workbench/server-review-session.d.ts +3 -1
  30. package/dist/src/review-workbench/server-review-session.js +1 -0
  31. package/dist/src/reviewed-candidate-resolution.js +13 -7
  32. package/dist/src/schema-mapping.d.ts +23 -0
  33. package/dist/src/schema-mapping.js +30 -20
  34. package/dist/src/to-surface.d.ts +30 -6
  35. package/dist/src/to-surface.js +196 -18
  36. package/dist/src/types.d.ts +39 -0
  37. package/package.json +5 -4
@@ -1,6 +1,6 @@
1
1
  import type { ReviewSessionEvent } from "../review-resource.js";
2
2
  import { type ReviewQueueSessionState } from "./review-queue-session.js";
3
- export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "decision-status-mismatch" | "decision-resolution-mismatch" | "missing-resolution-reason";
3
+ export type ReviewSessionReplayIssueCode = "invalid-sequence" | "duplicate-sequence" | "non-contiguous-sequence" | "unknown-active-item" | "unknown-review-item" | "unknown-candidate" | "missing-review-item" | "invalid-workbench-decision" | "decision-candidate-mismatch" | "decision-status-mismatch" | "decision-resolution-mismatch" | "missing-resolution-reason" | "edited-value-not-editable" | "edited-value-type-mismatch";
4
4
  export interface ReviewSessionReplayIssue {
5
5
  readonly code: ReviewSessionReplayIssueCode;
6
6
  readonly eventName: string;
@@ -10,3 +10,37 @@ export interface ReviewSessionReplayIssue {
10
10
  readonly message: string;
11
11
  }
12
12
  export declare function validateReviewSessionEventsForSnapshot(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewSessionReplayIssue[];
13
+ /**
14
+ * A ReviewSessionEvent whose `data.workbenchEditedValue` was rewritten from
15
+ * legacy editor text to its descriptor-typed value during replay (see
16
+ * checkEditedValueForItem). Not an error: the apply result still succeeds, and
17
+ * the warning lets a consumer see that a saved session was normalized.
18
+ */
19
+ export type ReviewSessionReplayWarning = {
20
+ readonly code: "edited-value-converted-from-text";
21
+ readonly eventName: string;
22
+ readonly sequence: number;
23
+ readonly reviewItemName: string;
24
+ readonly originalValue: string;
25
+ readonly convertedValue: unknown;
26
+ readonly message: string;
27
+ } | {
28
+ /**
29
+ * An edit that would be refused (see checkEditedValueForItem) on a
30
+ * decision event that a later decision for the same item superseded. It
31
+ * never reaches effectiveValue, so it does not fail the session.
32
+ */
33
+ readonly code: "superseded-edited-value-refused";
34
+ readonly eventName: string;
35
+ readonly sequence: number;
36
+ readonly reviewItemName: string;
37
+ readonly refusedCode: "edited-value-not-editable" | "edited-value-type-mismatch";
38
+ readonly editedValue: unknown;
39
+ readonly message: string;
40
+ };
41
+ /**
42
+ * Lists the legacy text edits replay converts to typed values, and refused
43
+ * edits that a later decision superseded. Call it only on an event stream that
44
+ * validateReviewSessionEventsForSnapshot accepted.
45
+ */
46
+ export declare function reviewSessionReplayWarningsForSnapshot(snapshot: ReviewQueueSessionState, events: readonly ReviewSessionEvent[]): ReviewSessionReplayWarning[];
@@ -1,7 +1,9 @@
1
1
  import { candidateForDecision, isClearedWorkbenchDecisionEvent, workbenchDecisionDefinitions, } from "./review-queue-session.js";
2
+ import { checkEditedValueForItem } from "./edited-value.js";
2
3
  export function validateReviewSessionEventsForSnapshot(snapshot, events) {
3
4
  const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
4
5
  const sequenceIssues = validateEventSequence(events);
6
+ const finalDecisionEvents = finalDecisionEventPerItem(events);
5
7
  const replayIssues = events.flatMap((event) => {
6
8
  const issues = [];
7
9
  const activeItemName = event.spec.activeItemName;
@@ -86,6 +88,22 @@ export function validateReviewSessionEventsForSnapshot(snapshot, events) {
86
88
  message: `ReviewSessionEvent ${event.metadata.name} decision ${decision} expects resolution ${expectedResolution ?? "none"}, but references ${event.spec.resolution ?? "no resolution"}.`,
87
89
  });
88
90
  }
91
+ const editedValue = editedValueFromDecisionEvent(event);
92
+ // Only the item's final decision decides its effectiveValue, so only an
93
+ // edit on that event is refused. An edit a later decision superseded is
94
+ // reported as a warning instead of failing the whole session.
95
+ if (item && decision === "accept-proposed" && editedValue !== undefined && finalDecisionEvents.has(event)) {
96
+ const check = checkEditedValueForItem(item, editedValue);
97
+ if (!check.ok) {
98
+ issues.push({
99
+ ...eventRef,
100
+ code: check.code,
101
+ reviewItemName: itemName,
102
+ candidateId: event.spec.candidateId,
103
+ message: `ReviewSessionEvent ${event.metadata.name}: ${check.message}`,
104
+ });
105
+ }
106
+ }
89
107
  if (decision === "could-not-confirm" && !event.spec.resolutionReason?.trim()) {
90
108
  issues.push({
91
109
  ...eventRef,
@@ -114,6 +132,65 @@ export function validateReviewSessionEventsForSnapshot(snapshot, events) {
114
132
  });
115
133
  return [...sequenceIssues, ...replayIssues];
116
134
  }
135
+ /**
136
+ * Lists the legacy text edits replay converts to typed values, and refused
137
+ * edits that a later decision superseded. Call it only on an event stream that
138
+ * validateReviewSessionEventsForSnapshot accepted.
139
+ */
140
+ export function reviewSessionReplayWarningsForSnapshot(snapshot, events) {
141
+ const itemsByName = new Map(snapshot.items.map((item) => [item.metadata.name, item]));
142
+ const finalDecisionEvents = finalDecisionEventPerItem(events);
143
+ return events.flatMap((event) => {
144
+ const itemName = event.spec.reviewItemName;
145
+ const item = itemName ? itemsByName.get(itemName) : undefined;
146
+ const editedValue = editedValueFromDecisionEvent(event);
147
+ if (!item || !itemName || editedValue === undefined
148
+ || replayableWorkbenchDecision(event.spec.data?.workbenchDecision) !== "accept-proposed") {
149
+ return [];
150
+ }
151
+ const check = checkEditedValueForItem(item, editedValue);
152
+ if (!check.ok) {
153
+ return finalDecisionEvents.has(event) ? [] : [{
154
+ code: "superseded-edited-value-refused",
155
+ eventName: event.metadata.name,
156
+ sequence: event.spec.sequence,
157
+ reviewItemName: itemName,
158
+ refusedCode: check.code,
159
+ editedValue,
160
+ message: `ReviewSessionEvent ${event.metadata.name} carries an edit a later decision superseded, which would be refused: ${check.message}`,
161
+ }];
162
+ }
163
+ if (!check.convertedFromText || typeof editedValue !== "string") {
164
+ return [];
165
+ }
166
+ return [{
167
+ code: "edited-value-converted-from-text",
168
+ eventName: event.metadata.name,
169
+ sequence: event.spec.sequence,
170
+ reviewItemName: itemName,
171
+ originalValue: editedValue,
172
+ convertedValue: check.value,
173
+ message: `ReviewSessionEvent ${event.metadata.name} stores edited value ${JSON.stringify(editedValue)} as text; replay converted it to ${JSON.stringify(check.value)} per ReviewItem ${itemName}'s value descriptor.`,
174
+ }];
175
+ });
176
+ }
177
+ /** The last decision event (by sequence, as replay applies them) for each ReviewItem. */
178
+ function finalDecisionEventPerItem(events) {
179
+ const lastByItem = new Map();
180
+ for (const event of [...events].sort((left, right) => left.spec.sequence - right.spec.sequence)) {
181
+ if ((event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
182
+ && event.spec.reviewItemName) {
183
+ lastByItem.set(event.spec.reviewItemName, event);
184
+ }
185
+ }
186
+ return new Set(lastByItem.values());
187
+ }
188
+ function editedValueFromDecisionEvent(event) {
189
+ return (event.spec.eventType === "decision-changed" || event.spec.eventType === "decision-submitted")
190
+ && event.spec.data && "workbenchEditedValue" in event.spec.data
191
+ ? event.spec.data.workbenchEditedValue
192
+ : undefined;
193
+ }
117
194
  function replayableWorkbenchDecision(value) {
118
195
  return typeof value === "string" && value in workbenchDecisionDefinitions
119
196
  ? value
@@ -1,5 +1,5 @@
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";
@@ -8,7 +8,8 @@ export { buildExtractionInspectorModel, exportExtractionInspector, filterExtract
8
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";
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;
@@ -207,8 +211,8 @@ export declare function buildReviewQueueWindow(session: ReviewQueueSessionState,
207
211
  * error message when the value violates the declared type/enum constraint, or
208
212
  * `undefined` when it is acceptable — including when there is no descriptor or
209
213
  * 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).
214
+ * FORMAT check on the editor text; on accept the workbench stores the text
215
+ * converted to the descriptor's JSON type (`editedValueFromEditorText`).
212
216
  */
213
217
  export declare function validateProposedValue(descriptor: ReviewValueDescriptor | undefined, rawValue: string): string | undefined;
214
218
  export declare function mountReviewWorkbench(root: HTMLElement, startState?: ReviewQueueSessionState | ReviewWorkbenchState, options?: MountReviewWorkbenchOptions): void;
@@ -1,12 +1,13 @@
1
1
  import { canonicalJson } from "./canonical.js";
2
2
  import { assertReviewResolutionConsistency } from "../producer-discipline.js";
3
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";
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";
@@ -14,7 +15,8 @@ export { buildExtractionInspectorModel, exportExtractionInspector, filterExtract
14
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
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) {
@@ -332,6 +334,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
332
334
  results: [],
333
335
  };
334
336
  }
337
+ const warnings = reviewSessionReplayWarningsForSnapshot(options.snapshot, options.events);
335
338
  const replayedSession = replayReviewSessionEvents(options.snapshot, options.events);
336
339
  const sessionExport = buildReviewWorkbenchSessionExport(replayedSession, options.events);
337
340
  const resolvedItemNames = new Set(sessionExport.results.map((result) => result.reviewItemName));
@@ -356,6 +359,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
356
359
  return {
357
360
  ok: false,
358
361
  issues,
362
+ warnings,
359
363
  unresolvedItemNames,
360
364
  replayedSession,
361
365
  sessionExport,
@@ -366,6 +370,7 @@ export function deriveReviewSessionApplyResultForSnapshot(options) {
366
370
  return {
367
371
  ok: true,
368
372
  issues: [],
373
+ warnings,
369
374
  unresolvedItemNames,
370
375
  replayedSession,
371
376
  sessionExport,
@@ -804,8 +809,8 @@ function toDateInputValue(text) {
804
809
  * error message when the value violates the declared type/enum constraint, or
805
810
  * `undefined` when it is acceptable — including when there is no descriptor or
806
811
  * 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).
812
+ * FORMAT check on the editor text; on accept the workbench stores the text
813
+ * converted to the descriptor's JSON type (`editedValueFromEditorText`).
809
814
  */
810
815
  export function validateProposedValue(descriptor, rawValue) {
811
816
  if (!descriptor)
@@ -815,7 +820,9 @@ export function validateProposedValue(descriptor, rawValue) {
815
820
  case "number":
816
821
  if (value === "")
817
822
  return "Enter a number.";
818
- return Number.isFinite(Number(value)) ? undefined : `"${rawValue}" is not a number.`;
823
+ return parsePlainDecimal(value) !== undefined
824
+ ? undefined
825
+ : `"${rawValue}" is not a number (use plain decimal digits: any safe integer, or a fraction of at most 15 significant digits).`;
819
826
  case "boolean":
820
827
  if (value === "")
821
828
  return "Choose true or false.";
@@ -823,7 +830,7 @@ export function validateProposedValue(descriptor, rawValue) {
823
830
  case "date":
824
831
  if (value === "")
825
832
  return "Enter a date.";
826
- return /^\d{4}-\d{2}-\d{2}$/.test(value) && !Number.isNaN(Date.parse(value))
833
+ return isIsoCalendarDate(value)
827
834
  ? undefined
828
835
  : `"${rawValue}" is not a valid date (YYYY-MM-DD).`;
829
836
  case "enum": {
@@ -1210,7 +1217,12 @@ function createReviewWorkbenchController(root, startState, options) {
1210
1217
  const originalText = proposed ? formatValue(proposed.value) : undefined;
1211
1218
  const nextEditedValuesByItemName = { ...session.editedValuesByItemName };
1212
1219
  if (decision === "accept-proposed" && rawEditedValue !== undefined && rawEditedValue !== originalText) {
1213
- nextEditedValuesByItemName[itemName] = rawEditedValue;
1220
+ // Store the edit as the descriptor's JSON type (a number field's "42" is
1221
+ // 42), so the decision event and effectiveValue carry a typed value the
1222
+ // server apply boundary accepts (kontourai/survey#278). Text that does not
1223
+ // parse is kept as typed; the apply boundary refuses it.
1224
+ const typedValue = editedValueFromEditorText(item?.spec.valueDescriptor, rawEditedValue);
1225
+ nextEditedValuesByItemName[itemName] = typedValue === undefined ? rawEditedValue : typedValue;
1214
1226
  }
1215
1227
  else {
1216
1228
  delete nextEditedValuesByItemName[itemName];
@@ -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
@@ -20,7 +20,7 @@
20
20
  * so that resolveInquiry can resolve across systems with weakest-link
21
21
  * capping.
22
22
  */
23
- import { evaluateAutoAccept, getProducerProposal, projectProposalsToCandidateSet, } from "./producer-profile.js";
23
+ import { assertValidAutoAcceptThreshold, evaluateAutoAccept, getProducerProposal, projectProposalsToCandidateSet, } from "./producer-profile.js";
24
24
  import { buildSurveyTrustBundle } from "./to-surface.js";
25
25
  // ---------------------------------------------------------------------------
26
26
  // Canonical pair key
@@ -35,6 +35,14 @@ function fieldPairKey(a, b) {
35
35
  function mappingSubjectId(a, b) {
36
36
  return fieldPairKey(a, b);
37
37
  }
38
+ function mappingValue(proposal) {
39
+ return {
40
+ relation: proposal.relation,
41
+ sourceField: proposal.sourceField,
42
+ targetField: proposal.targetField,
43
+ conversion: proposal.conversion,
44
+ };
45
+ }
38
46
  // ---------------------------------------------------------------------------
39
47
  // surveySchemaMapping
40
48
  // ---------------------------------------------------------------------------
@@ -56,9 +64,12 @@ function mappingSubjectId(a, b) {
56
64
  * storage) or for mappingReviewToSurface after human review.
57
65
  */
58
66
  export async function surveySchemaMapping(context, extractor, options = {}) {
67
+ if (options.autoAcceptMinConfidence !== undefined)
68
+ assertValidAutoAcceptThreshold(options.autoAcceptMinConfidence);
59
69
  const generatedAt = options.generatedAt ?? new Date().toISOString();
60
70
  const source = options.source ?? `schema-mapping:${extractor.name}`;
61
71
  const proposals = await Promise.resolve(extractor.extract(context));
72
+ const autoAcceptWarnings = [];
62
73
  // One RawSource per system schema
63
74
  const rawSources = context.systems.map((s) => ({
64
75
  id: `schema-mapping.source.${s.system}`,
@@ -93,11 +104,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
93
104
  id: extractionId,
94
105
  sourceId: rawSource.id,
95
106
  target: `${proposal.sourceField.entity}.${proposal.sourceField.field}:maps-to:${proposal.targetField.entity}.${proposal.targetField.field}`,
96
- value: {
97
- relation: proposal.relation,
98
- targetField: proposal.targetField,
99
- conversion: proposal.conversion,
100
- },
107
+ value: mappingValue(proposal),
101
108
  confidence: proposal.confidence,
102
109
  locator: proposal.sourceField.locator ?? `structured-field:${proposal.sourceField.entity}.${proposal.sourceField.field}`,
103
110
  excerpt: proposal.evidence.map((e) => `[${e.system}] ${e.excerpt}`).join(" | "),
@@ -120,11 +127,7 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
120
127
  return {
121
128
  candidateId: `schema-mapping.candidate.${proposal.id}`,
122
129
  extractionId,
123
- value: {
124
- relation: proposal.relation,
125
- targetField: proposal.targetField,
126
- conversion: proposal.conversion,
127
- },
130
+ value: mappingValue(proposal),
128
131
  confidence: proposal.confidence,
129
132
  equivalenceKey: proposal.relation,
130
133
  metadata: {
@@ -179,6 +182,13 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
179
182
  rationale: selectedProposal?.rationale,
180
183
  proposedAt: selectedProposal?.proposedAt,
181
184
  }, hasConflict, { minConfidence: options.autoAcceptMinConfidence }, generatedAt);
185
+ if (decision.warning) {
186
+ autoAcceptWarnings.push({
187
+ code: decision.warning,
188
+ proposalId: selectedProposal?.proposalId ?? selectedCandidate.id,
189
+ confidence: decision.confidence,
190
+ });
191
+ }
182
192
  if (decision.accepted) {
183
193
  const reviewId = `schema-mapping.review.${pairKey}`;
184
194
  reviewOutcomes.push({
@@ -208,12 +218,8 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
208
218
  facet: "schema-mapping.profile",
209
219
  claimType: "schema-mapping.field-link",
210
220
  fieldOrBehavior: "maps-to",
211
- value: {
212
- relation: first.relation,
213
- sourceField: first.sourceField,
214
- targetField: first.targetField,
215
- conversion: first.conversion,
216
- },
221
+ // No value override: the claim carries the selected candidate's value,
222
+ // which is the whole mapping a reviewer (or auto-accept) decided on.
217
223
  ...(claimStatus ? { status: claimStatus } : {}),
218
224
  impactLevel: "medium",
219
225
  collectedBy: extractor.name,
@@ -241,7 +247,12 @@ export async function surveySchemaMapping(context, extractor, options = {}) {
241
247
  reviewOutcomes,
242
248
  claims,
243
249
  };
244
- return { surveyInput, proposals, candidateSets };
250
+ return {
251
+ surveyInput,
252
+ proposals,
253
+ candidateSets,
254
+ ...(autoAcceptWarnings.length > 0 ? { autoAcceptWarnings } : {}),
255
+ };
245
256
  }
246
257
  // ---------------------------------------------------------------------------
247
258
  // mappingReviewToSurface
@@ -315,7 +326,7 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
315
326
  id: extractionId,
316
327
  sourceId: rawSourceId,
317
328
  target: `${sourceField.entity}.${sourceField.field}:maps-to:${targetField.entity}.${targetField.field}`,
318
- value: { relation, targetField, conversion },
329
+ value: { relation, sourceField, targetField, conversion },
319
330
  confidence,
320
331
  locator: sourceField.locator ?? `structured-field:${sourceField.entity}.${sourceField.field}`,
321
332
  excerpt: evidence.map((e) => `[${e.system}] ${e.excerpt}`).join(" | "),
@@ -352,7 +363,6 @@ export function mappingReviewToSurface(reviewedMappings, options = {}) {
352
363
  facet: "schema-mapping.profile",
353
364
  claimType: "schema-mapping.field-link",
354
365
  fieldOrBehavior: "maps-to",
355
- value: { relation, sourceField, targetField, conversion },
356
366
  status: rm.reviewOutcome.status,
357
367
  impactLevel: "medium",
358
368
  collectedBy: proposedBy,
@@ -2,6 +2,14 @@ import type { TrustBundle } from "@kontourai/surface";
2
2
  import { type CalibrationMetrics } from "./calibration.js";
3
3
  import type { SurveyInput } from "./types.js";
4
4
  export interface SurveyCalibrationOptions {
5
+ /**
6
+ * EXPERIMENTAL opt-in, required for any `conclusionConfidence.value` to be
7
+ * set (#279). The value is the affirmation rate of the claim's whole
8
+ * extractor/field GROUP — a base rate every affirmed claim in the group
9
+ * shares — not a per-claim probability. Without this flag the `calibration`
10
+ * option sets no value.
11
+ */
12
+ experimentalConclusionValue?: boolean;
5
13
  /**
6
14
  * Precomputed calibration to source the value from — typically derived over a
7
15
  * LONGER history than the current batch (a better-grounded curve, and it avoids
@@ -30,16 +38,32 @@ export interface BuildSurveyTrustBundleOptions {
30
38
  */
31
39
  projectionContextId?: string;
32
40
  /**
33
- * Populate `conclusionConfidence.value` from empirical review calibration —
34
- * "how often this extractor's proposals at this confidence were affirmed by a
35
- * human reviewer" (the produce side of the confidence loop; see #114/#137).
36
- * `true` derives calibration from this batch; an object supplies precomputed
37
- * metrics and/or a `minSamples` floor. Absent → `value` stays unset and only
38
- * the comfort-zone signal is carried (unchanged behavior).
41
+ * EXPERIMENTAL. Populate `conclusionConfidence.value` from empirical review
42
+ * calibration — the affirmation rate of the claim's extractor/field group
43
+ * (a group base rate, not a per-claim probability; see #114/#137/#279).
44
+ * A value is set ONLY with `{ experimentalConclusionValue: true }`; `true` or
45
+ * an object without that flag sets no value. The object may also supply
46
+ * precomputed `metrics` and/or a `minSamples` floor. Absent → `value` stays
47
+ * unset and only the comfort-zone signal is carried.
39
48
  *
40
49
  * ADVISORY (ADR 0003 §4): this only enriches the emitted conclusion confidence;
41
50
  * it never changes a claim's `status`.
42
51
  */
43
52
  calibration?: boolean | SurveyCalibrationOptions;
44
53
  }
54
+ /**
55
+ * Thrown when a claim's trusted status or value does not agree with the review
56
+ * outcome it cites, or when the governing review cannot be chosen.
57
+ * - `status-mismatch`: a `verified`/`assumed` claim cites a review whose status differs.
58
+ * - `value-mismatch`: a `verified`/`assumed` claim carries a value other than the reviewed value.
59
+ * - `ambiguous-review-order`: several reviews apply to one candidate and the latest
60
+ * cannot be determined from `reviewedAt` (missing, unparseable, or tied).
61
+ */
62
+ export declare class ReviewAgreementError extends Error {
63
+ readonly name = "ReviewAgreementError";
64
+ readonly code: "status-mismatch" | "value-mismatch" | "ambiguous-review-order";
65
+ constructor(code: ReviewAgreementError["code"], message: string);
66
+ }
45
67
  export declare function buildSurveyTrustBundle(input: SurveyInput, options?: BuildSurveyTrustBundleOptions): TrustBundle;
68
+ /** Test-only: re-arm the once-per-process calibration opt-in warning. Not exported from the package index. */
69
+ export declare function resetCalibrationOptInWarningForTests(): void;